@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/src/validation.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
udlDocumentSchema,
|
|
3
|
+
subjectPartyRoles,
|
|
4
|
+
sameObjectField,
|
|
3
5
|
MAX_ACTION_EXPANSION,
|
|
4
6
|
type UdlDocument,
|
|
5
7
|
type UdlField,
|
|
@@ -7,11 +9,63 @@ import {
|
|
|
7
9
|
type UdlValue,
|
|
8
10
|
type UdlCalculation,
|
|
9
11
|
type UdlSelection,
|
|
12
|
+
type UdlAction,
|
|
13
|
+
type UdlActionSubject,
|
|
14
|
+
type UdlSubjectRequirement,
|
|
15
|
+
type UdlObjectField,
|
|
16
|
+
type UdlObjectKind,
|
|
10
17
|
} from "./schema.js";
|
|
11
18
|
import { issue, type UdlIssue } from "./diagnostics.js";
|
|
12
19
|
import { validateReportDefinition } from "./reporting-validation.js";
|
|
13
20
|
import { analyzeInstrumentFinance } from "./finance.js";
|
|
14
21
|
|
|
22
|
+
export const RESERVED_OBJECT_NAMES = [
|
|
23
|
+
"objectId",
|
|
24
|
+
"externalId",
|
|
25
|
+
"revision",
|
|
26
|
+
"kind",
|
|
27
|
+
"tenantId",
|
|
28
|
+
"productId",
|
|
29
|
+
] as const;
|
|
30
|
+
const reservedObjectNamesSet = new Set<string>(RESERVED_OBJECT_NAMES);
|
|
31
|
+
function hasParty(
|
|
32
|
+
document: UdlDocument,
|
|
33
|
+
instrument: UdlInstrument | undefined,
|
|
34
|
+
name: string,
|
|
35
|
+
): boolean {
|
|
36
|
+
if (document.parties[name]) return true;
|
|
37
|
+
if (!instrument?.subject) return false;
|
|
38
|
+
return (
|
|
39
|
+
document.objects
|
|
40
|
+
.find((kind) => kind.id === instrument.subject)
|
|
41
|
+
?.attachments.some(
|
|
42
|
+
(attachment) =>
|
|
43
|
+
(attachment.instrument === instrument.id ||
|
|
44
|
+
instrument.id.startsWith(`${attachment.instrument}_`)) &&
|
|
45
|
+
Object.values(attachment.parties).some((binding) =>
|
|
46
|
+
"role" in binding ? binding.role === name : binding.party === name,
|
|
47
|
+
),
|
|
48
|
+
) ?? false
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function moneyParty(
|
|
53
|
+
document: UdlDocument,
|
|
54
|
+
instrument: UdlInstrument | undefined,
|
|
55
|
+
name: string,
|
|
56
|
+
): boolean {
|
|
57
|
+
return (
|
|
58
|
+
hasParty(document, instrument, name) &&
|
|
59
|
+
(document.parties[name]?.kind === "business" ||
|
|
60
|
+
(!document.parties[name] && !!instrument?.subject) ||
|
|
61
|
+
(!instrument?.subject && document.parties[name]?.kind === "person"))
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
function approvalParty(document: UdlDocument, name: string): boolean {
|
|
65
|
+
const party = document.parties[name];
|
|
66
|
+
return party?.kind === "staff" && !!party.role;
|
|
67
|
+
}
|
|
68
|
+
|
|
15
69
|
export type UdlValidationResult =
|
|
16
70
|
| { ok: true; value: UdlDocument }
|
|
17
71
|
| { ok: false; issues: readonly UdlIssue[] };
|
|
@@ -58,7 +112,9 @@ const overlap = (a: string | string[], b: string | string[]) =>
|
|
|
58
112
|
targetIds(a).some((id) => targetIds(b).includes(id));
|
|
59
113
|
|
|
60
114
|
/** Union paths retain only fields with compatible types on every member. */
|
|
61
|
-
function commonField(
|
|
115
|
+
function commonField(
|
|
116
|
+
fields: (UdlField | UdlObjectField | undefined)[],
|
|
117
|
+
): UdlField | undefined {
|
|
62
118
|
const first = fields[0];
|
|
63
119
|
if (
|
|
64
120
|
!first ||
|
|
@@ -71,6 +127,16 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
|
|
|
71
127
|
)
|
|
72
128
|
return;
|
|
73
129
|
if (first.type === "ref") {
|
|
130
|
+
if (
|
|
131
|
+
fields.some(
|
|
132
|
+
(field) =>
|
|
133
|
+
field?.type !== "ref" ||
|
|
134
|
+
field.targetKind !== first.targetKind ||
|
|
135
|
+
JSON.stringify(field.targetFamily) !==
|
|
136
|
+
JSON.stringify(first.targetFamily),
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
return;
|
|
74
140
|
const targets = [
|
|
75
141
|
...new Set(
|
|
76
142
|
fields.flatMap((field) =>
|
|
@@ -78,7 +144,12 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
|
|
|
78
144
|
),
|
|
79
145
|
),
|
|
80
146
|
];
|
|
81
|
-
return {
|
|
147
|
+
return {
|
|
148
|
+
...first,
|
|
149
|
+
targetKind: first.targetKind,
|
|
150
|
+
target: targets.length === 1 ? targets[0]! : targets,
|
|
151
|
+
...(first.targetFamily ? { targetFamily: first.targetFamily } : {}),
|
|
152
|
+
} as UdlField;
|
|
82
153
|
}
|
|
83
154
|
if (
|
|
84
155
|
first.type === "account" &&
|
|
@@ -88,7 +159,6 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
|
|
|
88
159
|
field.book !== first.book ||
|
|
89
160
|
field.owner !== first.owner ||
|
|
90
161
|
field.key !== first.key ||
|
|
91
|
-
field.external !== first.external ||
|
|
92
162
|
field.contra !== first.contra,
|
|
93
163
|
)
|
|
94
164
|
)
|
|
@@ -109,11 +179,12 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
|
|
|
109
179
|
(field) =>
|
|
110
180
|
field?.type !== "list" ||
|
|
111
181
|
field.item !== first.item ||
|
|
182
|
+
field.targetKind !== first.targetKind ||
|
|
112
183
|
field.target !== first.target,
|
|
113
184
|
)
|
|
114
185
|
)
|
|
115
186
|
return;
|
|
116
|
-
return first;
|
|
187
|
+
return first as UdlField;
|
|
117
188
|
}
|
|
118
189
|
|
|
119
190
|
export function resolveField(
|
|
@@ -121,8 +192,71 @@ export function resolveField(
|
|
|
121
192
|
instrument: UdlInstrument,
|
|
122
193
|
path: string,
|
|
123
194
|
input: readonly UdlField[] = [],
|
|
195
|
+
action?: UdlAction | UdlActionSubject,
|
|
196
|
+
): UdlField | undefined {
|
|
197
|
+
return resolvePath(document, instrument, path, input, new Map(), action);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** A subject expression names a requirement; objectField names its stored metadata. */
|
|
201
|
+
export function resolveSubjectRequirement(
|
|
202
|
+
instrument: UdlInstrument,
|
|
203
|
+
name: string,
|
|
204
|
+
action?: UdlAction | UdlActionSubject,
|
|
205
|
+
): UdlSubjectRequirement | undefined {
|
|
206
|
+
const requirements = action
|
|
207
|
+
? "requirements" in action
|
|
208
|
+
? action.requirements
|
|
209
|
+
: action.subject?.requirements
|
|
210
|
+
: Object.values(instrument.actions).flatMap(
|
|
211
|
+
(item) => item.subject?.requirements ?? [],
|
|
212
|
+
);
|
|
213
|
+
return requirements?.find((requirement) => requirement.field.name === name);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function resolveReferencePath(
|
|
217
|
+
document: UdlDocument,
|
|
218
|
+
field: Extract<UdlField, { type: "ref" }>,
|
|
219
|
+
suffix: string,
|
|
220
|
+
cache: Map<string, UdlField | undefined>,
|
|
221
|
+
): UdlField | undefined {
|
|
222
|
+
return commonField(
|
|
223
|
+
targetIds(field.target).map((id) => {
|
|
224
|
+
const key = `${field.targetKind}:${id}:${suffix}`;
|
|
225
|
+
if (!cache.has(key)) {
|
|
226
|
+
const object =
|
|
227
|
+
field.targetKind === "object" &&
|
|
228
|
+
document.objects.find((item) => item.id === id);
|
|
229
|
+
const instrument =
|
|
230
|
+
field.targetKind === "instrument" &&
|
|
231
|
+
document.instruments.find((item) => item.id === id);
|
|
232
|
+
cache.set(
|
|
233
|
+
key,
|
|
234
|
+
object
|
|
235
|
+
? resolveObjectPath(document, object, suffix, cache)
|
|
236
|
+
: instrument
|
|
237
|
+
? resolvePath(document, instrument, `self.${suffix}`, [], cache)
|
|
238
|
+
: undefined,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
return cache.get(key);
|
|
242
|
+
}),
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function resolveObjectPath(
|
|
247
|
+
document: UdlDocument,
|
|
248
|
+
object: UdlObjectKind,
|
|
249
|
+
path: string,
|
|
250
|
+
cache: Map<string, UdlField | undefined>,
|
|
124
251
|
): UdlField | undefined {
|
|
125
|
-
|
|
252
|
+
const parts = path.split(".");
|
|
253
|
+
const key = parts.shift();
|
|
254
|
+
if (!key) return;
|
|
255
|
+
const field = object.fields.find((f) => f.name === key);
|
|
256
|
+
if (!field) return;
|
|
257
|
+
if (parts.length === 0) return field as UdlField;
|
|
258
|
+
if (field.type !== "ref") return;
|
|
259
|
+
return resolveReferencePath(document, field, parts.join("."), cache);
|
|
126
260
|
}
|
|
127
261
|
|
|
128
262
|
function resolvePath(
|
|
@@ -131,30 +265,73 @@ function resolvePath(
|
|
|
131
265
|
path: string,
|
|
132
266
|
input: readonly UdlField[],
|
|
133
267
|
cache: Map<string, UdlField | undefined>,
|
|
268
|
+
action?: UdlAction | UdlActionSubject,
|
|
134
269
|
): UdlField | undefined {
|
|
135
270
|
const parts = path.split(".");
|
|
136
271
|
const root = parts.shift();
|
|
137
272
|
const key = parts.shift();
|
|
138
273
|
if (!key)
|
|
139
274
|
return root === "self"
|
|
140
|
-
? {
|
|
275
|
+
? {
|
|
276
|
+
name: "id",
|
|
277
|
+
type: "ref",
|
|
278
|
+
targetKind: "instrument",
|
|
279
|
+
target: instrument.id,
|
|
280
|
+
}
|
|
141
281
|
: undefined;
|
|
142
282
|
if (root === "party") {
|
|
143
|
-
if (!document
|
|
283
|
+
if (!moneyParty(document, instrument, key)) return;
|
|
144
284
|
if (parts.length === 0)
|
|
145
285
|
return { name: key, type: "account", owner: key, book: "cash" };
|
|
146
286
|
if (parts.length === 1 && ["balance", "reserved"].includes(parts[0]!))
|
|
147
287
|
return { name: parts[0]!, type: "money" };
|
|
148
288
|
return;
|
|
149
289
|
}
|
|
290
|
+
if (root === "self" && key === "subject" && instrument.subject) {
|
|
291
|
+
if (parts.length === 0)
|
|
292
|
+
return {
|
|
293
|
+
name: "subject",
|
|
294
|
+
type: "ref",
|
|
295
|
+
targetKind: "object",
|
|
296
|
+
target: instrument.subject,
|
|
297
|
+
};
|
|
298
|
+
const target = document.objects.find((o) => o.id === instrument.subject);
|
|
299
|
+
if (!target) return;
|
|
300
|
+
const suffix = parts.join(".");
|
|
301
|
+
const cacheKey = `obj:${instrument.subject}:${suffix}`;
|
|
302
|
+
if (!cache.has(cacheKey))
|
|
303
|
+
cache.set(cacheKey, resolveObjectPath(document, target, suffix, cache));
|
|
304
|
+
return cache.get(cacheKey);
|
|
305
|
+
}
|
|
150
306
|
if (root === "self" && parts.length === 0) {
|
|
151
|
-
if (key === "id")
|
|
307
|
+
if (key === "id")
|
|
308
|
+
return {
|
|
309
|
+
name: key,
|
|
310
|
+
type: "ref",
|
|
311
|
+
targetKind: "instrument",
|
|
312
|
+
target: instrument.id,
|
|
313
|
+
};
|
|
152
314
|
if (key === "now" || key === "createdAt")
|
|
153
315
|
return { name: key, type: "date" };
|
|
154
316
|
if (key === "productRevision") return { name: key, type: "text" };
|
|
155
317
|
if (key === "status")
|
|
156
318
|
return { name: key, type: "enum", values: instrument.lifecycle.states };
|
|
157
319
|
}
|
|
320
|
+
if (root === "subject") {
|
|
321
|
+
const requirement = resolveSubjectRequirement(instrument, key, action);
|
|
322
|
+
if (!requirement || !instrument.subject) return;
|
|
323
|
+
const object = document.objects.find(
|
|
324
|
+
(item) => item.id === instrument.subject,
|
|
325
|
+
);
|
|
326
|
+
if (!object) return;
|
|
327
|
+
return resolveObjectPath(
|
|
328
|
+
document,
|
|
329
|
+
object,
|
|
330
|
+
[requirement.objectField ?? requirement.field.name, ...parts].join("."),
|
|
331
|
+
cache,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
158
335
|
const scope = instrument;
|
|
159
336
|
let field = (
|
|
160
337
|
root === "self" ? instrument.fields : root === "input" ? input : []
|
|
@@ -180,18 +357,11 @@ function resolvePath(
|
|
|
180
357
|
values: ["reserved", "posted", "settled", "reversed", "voided"],
|
|
181
358
|
};
|
|
182
359
|
if (field?.type !== "ref") return;
|
|
183
|
-
return
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (!target) return;
|
|
189
|
-
const suffix = "self." + parts.slice(index).join(".");
|
|
190
|
-
const key = `${id}:${suffix}`;
|
|
191
|
-
if (!cache.has(key))
|
|
192
|
-
cache.set(key, resolvePath(document, target, suffix, [], cache));
|
|
193
|
-
return cache.get(key);
|
|
194
|
-
}),
|
|
360
|
+
return resolveReferencePath(
|
|
361
|
+
document,
|
|
362
|
+
field,
|
|
363
|
+
parts.slice(index).join("."),
|
|
364
|
+
cache,
|
|
195
365
|
);
|
|
196
366
|
}
|
|
197
367
|
return field;
|
|
@@ -238,10 +408,237 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
238
408
|
seen.add(v);
|
|
239
409
|
}
|
|
240
410
|
};
|
|
411
|
+
if (document.objects.length === 0 && document.instruments.length === 0)
|
|
412
|
+
add("$", "document requires at least one object or instrument", "UDL2002");
|
|
413
|
+
|
|
414
|
+
duplicate(
|
|
415
|
+
document.objects.map((o) => o.id),
|
|
416
|
+
"$.objects",
|
|
417
|
+
);
|
|
241
418
|
duplicate(
|
|
242
419
|
document.instruments.map((i) => i.id),
|
|
243
420
|
"$.instruments",
|
|
244
421
|
);
|
|
422
|
+
|
|
423
|
+
for (const name of subjectPartyRoles)
|
|
424
|
+
if (document.parties[name])
|
|
425
|
+
add(
|
|
426
|
+
`$.parties.${name}`,
|
|
427
|
+
`${name} is a reserved subject role`,
|
|
428
|
+
"party_name_reserved",
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
const byObjectId = new Map(document.objects.map((o) => [o.id, o]));
|
|
432
|
+
const byId = new Map(document.instruments.map((i) => [i.id, i]));
|
|
433
|
+
|
|
434
|
+
const checkFields = (
|
|
435
|
+
fields: readonly (UdlField | UdlObjectField)[],
|
|
436
|
+
where: string,
|
|
437
|
+
isObject = false,
|
|
438
|
+
instrument?: UdlInstrument,
|
|
439
|
+
) => {
|
|
440
|
+
duplicate(
|
|
441
|
+
fields.map((f) => f.name),
|
|
442
|
+
where,
|
|
443
|
+
);
|
|
444
|
+
for (const f of fields) {
|
|
445
|
+
if (isObject) {
|
|
446
|
+
if (reservedObjectNamesSet.has(f.name))
|
|
447
|
+
add(where, `${f.name} is a reserved object name`);
|
|
448
|
+
} else {
|
|
449
|
+
if (f.type === "account" && f.contra && f.book !== "claim")
|
|
450
|
+
add(where, "contra accounts require the claim book");
|
|
451
|
+
if (
|
|
452
|
+
["id", "status", "createdAt", "now", "productRevision"].includes(
|
|
453
|
+
f.name,
|
|
454
|
+
)
|
|
455
|
+
)
|
|
456
|
+
add(where, `${f.name} is a sealed instance field`);
|
|
457
|
+
if (
|
|
458
|
+
f.type === "account" &&
|
|
459
|
+
f.owner !== "self" &&
|
|
460
|
+
!moneyParty(document, instrument, f.owner)
|
|
461
|
+
)
|
|
462
|
+
add(
|
|
463
|
+
where,
|
|
464
|
+
`${f.name} needs an eligible money party as owner`,
|
|
465
|
+
hasParty(document, instrument, f.owner)
|
|
466
|
+
? "party_kind_mismatch"
|
|
467
|
+
: "subject_party_unbound",
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
if (f.type === "text") {
|
|
471
|
+
if ((f.minLength ?? 1) > (f.maxLength ?? 2048))
|
|
472
|
+
add(where, "text minimum exceeds maximum");
|
|
473
|
+
if (f.pattern) {
|
|
474
|
+
try {
|
|
475
|
+
new RegExp(f.pattern);
|
|
476
|
+
} catch {
|
|
477
|
+
add(where, "invalid text character class");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (f.type === "ref") {
|
|
482
|
+
duplicate(targetIds(f.target), where);
|
|
483
|
+
for (const id of targetIds(f.target)) {
|
|
484
|
+
const exists =
|
|
485
|
+
f.targetKind === "object" ? byObjectId.has(id) : byId.has(id);
|
|
486
|
+
if (!exists) add(where, `unknown reference target ${id}`, "UDL5001");
|
|
487
|
+
}
|
|
488
|
+
if (f.targetFamily) {
|
|
489
|
+
if (f.targetKind !== "instrument") {
|
|
490
|
+
add(
|
|
491
|
+
where,
|
|
492
|
+
"targetFamily requires an instrument reference target",
|
|
493
|
+
"UDL5001",
|
|
494
|
+
);
|
|
495
|
+
} else {
|
|
496
|
+
for (const id of targetIds(f.target)) {
|
|
497
|
+
const targetInst = byId.get(id);
|
|
498
|
+
if (
|
|
499
|
+
targetInst &&
|
|
500
|
+
(!targetInst.family ||
|
|
501
|
+
targetInst.family.module !== f.targetFamily.module ||
|
|
502
|
+
targetInst.family.exportPath !== f.targetFamily.exportPath ||
|
|
503
|
+
targetInst.family.revision !== f.targetFamily.revision)
|
|
504
|
+
) {
|
|
505
|
+
add(
|
|
506
|
+
where,
|
|
507
|
+
`targetFamily does not match target instrument ${id} family`,
|
|
508
|
+
"UDL5001",
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (f.type === "enum") {
|
|
516
|
+
duplicate(f.values, where);
|
|
517
|
+
if (f.value !== undefined && !f.values.includes(f.value))
|
|
518
|
+
add(where, `${f.name} constant is outside its enum`);
|
|
519
|
+
}
|
|
520
|
+
if (f.type === "list") {
|
|
521
|
+
const validRefList =
|
|
522
|
+
f.item === "ref" &&
|
|
523
|
+
f.targetKind &&
|
|
524
|
+
(f.targetKind === "object"
|
|
525
|
+
? byObjectId.has(f.target ?? "")
|
|
526
|
+
: byId.has(f.target ?? ""));
|
|
527
|
+
const nonRefTarget =
|
|
528
|
+
f.item !== "ref" &&
|
|
529
|
+
(f.targetKind !== undefined || f.target !== undefined);
|
|
530
|
+
if (!validRefList && (f.item === "ref" || nonRefTarget))
|
|
531
|
+
add(
|
|
532
|
+
where,
|
|
533
|
+
`${f.name} needs a target exactly when its items are references`,
|
|
534
|
+
);
|
|
535
|
+
if (f.targetFamily) {
|
|
536
|
+
if (f.item !== "ref" || f.targetKind !== "instrument") {
|
|
537
|
+
add(
|
|
538
|
+
where,
|
|
539
|
+
"targetFamily requires an instrument reference target",
|
|
540
|
+
"UDL5001",
|
|
541
|
+
);
|
|
542
|
+
} else if (f.target) {
|
|
543
|
+
const targetInst = byId.get(f.target);
|
|
544
|
+
if (
|
|
545
|
+
targetInst &&
|
|
546
|
+
(!targetInst.family ||
|
|
547
|
+
targetInst.family.module !== f.targetFamily.module ||
|
|
548
|
+
targetInst.family.exportPath !== f.targetFamily.exportPath ||
|
|
549
|
+
targetInst.family.revision !== f.targetFamily.revision)
|
|
550
|
+
) {
|
|
551
|
+
add(
|
|
552
|
+
where,
|
|
553
|
+
`targetFamily does not match target instrument ${f.target} family`,
|
|
554
|
+
"UDL5001",
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
if (
|
|
561
|
+
(f.type === "integer" || f.type === "money") &&
|
|
562
|
+
f.minimum !== undefined &&
|
|
563
|
+
f.maximum !== undefined &&
|
|
564
|
+
BigInt(f.minimum) > BigInt(f.maximum)
|
|
565
|
+
)
|
|
566
|
+
add(where, `${f.name} minimum exceeds maximum`);
|
|
567
|
+
if (
|
|
568
|
+
(f.type === "integer" || f.type === "money") &&
|
|
569
|
+
f.value !== undefined &&
|
|
570
|
+
((f.minimum !== undefined && BigInt(f.value) < BigInt(f.minimum)) ||
|
|
571
|
+
(f.maximum !== undefined && BigInt(f.value) > BigInt(f.maximum)))
|
|
572
|
+
)
|
|
573
|
+
add(where, `${f.name} constant violates its bounds`);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
for (const [index, obj] of document.objects.entries()) {
|
|
578
|
+
const base = `$.objects[${index}]`;
|
|
579
|
+
if (reservedObjectNamesSet.has(obj.id))
|
|
580
|
+
add(base, `${obj.id} is a reserved object name`);
|
|
581
|
+
duplicate(
|
|
582
|
+
obj.attachments.map((attachment) => attachment.name),
|
|
583
|
+
`${base}.attachments`,
|
|
584
|
+
);
|
|
585
|
+
duplicate(
|
|
586
|
+
obj.attachments.map((attachment) => attachment.instrument),
|
|
587
|
+
`${base}.attachments`,
|
|
588
|
+
);
|
|
589
|
+
for (const [attachmentIndex, attachment] of obj.attachments.entries()) {
|
|
590
|
+
for (const [parameter, binding] of Object.entries(attachment.parties))
|
|
591
|
+
if ("party" in binding && !document.parties[binding.party])
|
|
592
|
+
add(
|
|
593
|
+
`${base}.attachments[${attachmentIndex}].parties.${parameter}`,
|
|
594
|
+
`unknown declared party ${binding.party}`,
|
|
595
|
+
"subject_party_unbound",
|
|
596
|
+
);
|
|
597
|
+
if (byId.get(attachment.instrument)?.subject !== obj.id)
|
|
598
|
+
add(
|
|
599
|
+
`${base}.attachments`,
|
|
600
|
+
`${attachment.instrument} must attach to ${obj.id}`,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
duplicate(obj.authoredFields, `${base}.authoredFields`);
|
|
604
|
+
duplicate(obj.columns, `${base}.columns`);
|
|
605
|
+
checkFields(obj.fields, `${base}.fields`, true);
|
|
606
|
+
for (let i = 0; i < obj.authoredFields.length; i++) {
|
|
607
|
+
const af = obj.authoredFields[i]!;
|
|
608
|
+
if (reservedObjectNamesSet.has(af))
|
|
609
|
+
add(`${base}.authoredFields`, `${af} is a reserved object name`);
|
|
610
|
+
if (!obj.fields.some((f) => f.name === af))
|
|
611
|
+
add(
|
|
612
|
+
`${base}.authoredFields`,
|
|
613
|
+
`authored field ${af} is not in declared fields`,
|
|
614
|
+
"subject_field_unknown",
|
|
615
|
+
);
|
|
616
|
+
if (obj.fields[i]?.name !== af)
|
|
617
|
+
add(
|
|
618
|
+
base,
|
|
619
|
+
`authored field ${af} must precede attached requirements in fields`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
for (const col of obj.columns) {
|
|
623
|
+
if (reservedObjectNamesSet.has(col))
|
|
624
|
+
add(`${base}.columns`, `${col} is a reserved object name`);
|
|
625
|
+
if (!obj.fields.some((f) => f.name === col))
|
|
626
|
+
add(
|
|
627
|
+
`${base}.columns`,
|
|
628
|
+
`column ${col} is not a declared field`,
|
|
629
|
+
"subject_field_unknown",
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
const pubActions = document.instruments
|
|
633
|
+
.filter((i) => i.subject === obj.id)
|
|
634
|
+
.flatMap((i) =>
|
|
635
|
+
Object.values(i.actions).flatMap((a) =>
|
|
636
|
+
a.publicAction ? [a.publicAction] : [],
|
|
637
|
+
),
|
|
638
|
+
);
|
|
639
|
+
duplicate(pubActions, `$.objects.${obj.id}.publicActions`);
|
|
640
|
+
}
|
|
641
|
+
|
|
245
642
|
const reportIds = new Set<string>();
|
|
246
643
|
for (const [index, instrument] of document.instruments.entries()) {
|
|
247
644
|
for (const report of instrument.reports ?? []) {
|
|
@@ -259,19 +656,34 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
259
656
|
}
|
|
260
657
|
}
|
|
261
658
|
}
|
|
262
|
-
|
|
659
|
+
|
|
263
660
|
const calls = new Map<string, { targets: string[]; count: number }[]>();
|
|
264
661
|
for (const [index, inst] of document.instruments.entries()) {
|
|
265
662
|
const base = `$.instruments[${index}]`;
|
|
266
|
-
|
|
267
|
-
|
|
663
|
+
if (inst.subject && !byObjectId.has(inst.subject))
|
|
664
|
+
add(base, `unknown subject object ${inst.subject}`);
|
|
665
|
+
const field = (
|
|
666
|
+
p: string,
|
|
667
|
+
input: readonly UdlField[] = [],
|
|
668
|
+
action?: UdlAction | UdlActionSubject,
|
|
669
|
+
) => resolveField(document, inst, p, input, action);
|
|
268
670
|
const expect = (
|
|
269
671
|
p: string,
|
|
270
672
|
type: UdlField["type"],
|
|
271
673
|
where: string,
|
|
272
674
|
input: readonly UdlField[] = [],
|
|
675
|
+
action?: UdlAction | UdlActionSubject,
|
|
273
676
|
) => {
|
|
274
|
-
const
|
|
677
|
+
const party = p.startsWith("party.") ? p.split(".")[1]! : undefined;
|
|
678
|
+
if (party && !moneyParty(document, inst, party))
|
|
679
|
+
add(
|
|
680
|
+
where,
|
|
681
|
+
`${party} cannot supply a money account`,
|
|
682
|
+
hasParty(document, inst, party)
|
|
683
|
+
? "party_kind_mismatch"
|
|
684
|
+
: "subject_party_unbound",
|
|
685
|
+
);
|
|
686
|
+
const found = field(p, input, action);
|
|
275
687
|
if (found?.type !== type)
|
|
276
688
|
add(where, `${p} must name a ${type} field`, "UDL5001");
|
|
277
689
|
return found;
|
|
@@ -281,9 +693,10 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
281
693
|
type: UdlField["type"],
|
|
282
694
|
where: string,
|
|
283
695
|
input: readonly UdlField[] = [],
|
|
696
|
+
action?: UdlAction | UdlActionSubject,
|
|
284
697
|
) => {
|
|
285
698
|
if ("field" in v) {
|
|
286
|
-
expect(v.field, type, where, input);
|
|
699
|
+
expect(v.field, type, where, input, action);
|
|
287
700
|
return;
|
|
288
701
|
}
|
|
289
702
|
const valid =
|
|
@@ -299,82 +712,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
299
712
|
: typeof v.literal === "string";
|
|
300
713
|
if (!valid) add(where, `literal must have type ${type}`);
|
|
301
714
|
};
|
|
302
|
-
|
|
303
|
-
duplicate(
|
|
304
|
-
fields.map((f) => f.name),
|
|
305
|
-
where,
|
|
306
|
-
);
|
|
307
|
-
for (const f of fields) {
|
|
308
|
-
if (
|
|
309
|
-
f.type === "account" &&
|
|
310
|
-
((f.contra && f.book !== "claim") ||
|
|
311
|
-
(f.external && (f.book !== "cash" || f.owner === "self")))
|
|
312
|
-
)
|
|
313
|
-
add(
|
|
314
|
-
where,
|
|
315
|
-
"contra accounts require the claim book; external accounts require a cash party owner",
|
|
316
|
-
);
|
|
317
|
-
if (
|
|
318
|
-
["id", "status", "createdAt", "now", "productRevision"].includes(
|
|
319
|
-
f.name,
|
|
320
|
-
)
|
|
321
|
-
)
|
|
322
|
-
add(where, `${f.name} is a sealed instance field`);
|
|
323
|
-
if (f.type === "text") {
|
|
324
|
-
if ((f.minLength ?? 1) > (f.maxLength ?? 2048))
|
|
325
|
-
add(where, "text minimum exceeds maximum");
|
|
326
|
-
if (f.pattern) {
|
|
327
|
-
try {
|
|
328
|
-
new RegExp(f.pattern);
|
|
329
|
-
} catch {
|
|
330
|
-
add(where, "invalid text character class");
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
if (f.type === "ref") {
|
|
335
|
-
duplicate(targetIds(f.target), where);
|
|
336
|
-
for (const id of targetIds(f.target))
|
|
337
|
-
if (!byId.has(id))
|
|
338
|
-
add(where, `unknown reference target ${id}`, "UDL5001");
|
|
339
|
-
}
|
|
340
|
-
if (
|
|
341
|
-
f.type === "account" &&
|
|
342
|
-
f.owner !== "self" &&
|
|
343
|
-
!document.parties[f.owner]
|
|
344
|
-
)
|
|
345
|
-
add(where, `${f.name} needs self or a declared party as owner`);
|
|
346
|
-
if (f.type === "enum") {
|
|
347
|
-
duplicate(f.values, where);
|
|
348
|
-
if (f.value !== undefined && !f.values.includes(f.value))
|
|
349
|
-
add(where, `${f.name} constant is outside its enum`);
|
|
350
|
-
}
|
|
351
|
-
if (
|
|
352
|
-
f.type === "list" &&
|
|
353
|
-
(f.item === "ref"
|
|
354
|
-
? !f.target || !byId.has(f.target)
|
|
355
|
-
: f.target !== undefined)
|
|
356
|
-
)
|
|
357
|
-
add(
|
|
358
|
-
where,
|
|
359
|
-
`${f.name} needs a target exactly when its items are references`,
|
|
360
|
-
);
|
|
361
|
-
if (
|
|
362
|
-
(f.type === "integer" || f.type === "money") &&
|
|
363
|
-
f.minimum !== undefined &&
|
|
364
|
-
f.maximum !== undefined &&
|
|
365
|
-
BigInt(f.minimum) > BigInt(f.maximum)
|
|
366
|
-
)
|
|
367
|
-
add(where, `${f.name} minimum exceeds maximum`);
|
|
368
|
-
if (
|
|
369
|
-
(f.type === "integer" || f.type === "money") &&
|
|
370
|
-
f.value !== undefined &&
|
|
371
|
-
((f.minimum !== undefined && BigInt(f.value) < BigInt(f.minimum)) ||
|
|
372
|
-
(f.maximum !== undefined && BigInt(f.value) > BigInt(f.maximum)))
|
|
373
|
-
)
|
|
374
|
-
add(where, `${f.name} constant violates its bounds`);
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
|
-
checkFields(inst.fields, `${base}.fields`);
|
|
715
|
+
checkFields(inst.fields, `${base}.fields`, false, inst);
|
|
378
716
|
const captures = new Set(
|
|
379
717
|
Object.values(inst.actions).flatMap((a) => [
|
|
380
718
|
...a.moves.flatMap((m) =>
|
|
@@ -409,9 +747,24 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
409
747
|
a &&
|
|
410
748
|
b &&
|
|
411
749
|
a.type === b.type &&
|
|
412
|
-
(a.type !== "ref" ||
|
|
750
|
+
(a.type !== "ref" ||
|
|
751
|
+
(b.type === "ref" &&
|
|
752
|
+
a.targetKind === b.targetKind &&
|
|
753
|
+
overlap(a.target, b.target)));
|
|
413
754
|
const anchor = field(selection.anchor, input);
|
|
414
755
|
for (const target of targets) {
|
|
756
|
+
if (
|
|
757
|
+
selection.family &&
|
|
758
|
+
(!target.family ||
|
|
759
|
+
target.family.module !== selection.family.module ||
|
|
760
|
+
target.family.exportPath !== selection.family.exportPath ||
|
|
761
|
+
target.family.revision !== selection.family.revision)
|
|
762
|
+
)
|
|
763
|
+
add(
|
|
764
|
+
where,
|
|
765
|
+
`selection family does not match target instrument ${target.id} family`,
|
|
766
|
+
"UDL5001",
|
|
767
|
+
);
|
|
415
768
|
if (
|
|
416
769
|
selection.window &&
|
|
417
770
|
target.fields.find((f) => f.name === selection.window!.field)
|
|
@@ -471,6 +824,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
471
824
|
calculations: readonly UdlCalculation[],
|
|
472
825
|
where: string,
|
|
473
826
|
input: readonly UdlField[] = [],
|
|
827
|
+
act?: UdlAction,
|
|
474
828
|
) => {
|
|
475
829
|
duplicate(
|
|
476
830
|
calculations.map((c) => c.target),
|
|
@@ -484,7 +838,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
484
838
|
const resultType =
|
|
485
839
|
c.op === "at"
|
|
486
840
|
? (() => {
|
|
487
|
-
const list = field(c.list, input);
|
|
841
|
+
const list = field(c.list, input, act);
|
|
488
842
|
return list?.type === "list" ? list.item : "text";
|
|
489
843
|
})()
|
|
490
844
|
: (c.op === "aggregate" && c.measure === "count") ||
|
|
@@ -499,11 +853,11 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
499
853
|
const operands: UdlValue[] = [];
|
|
500
854
|
const money = (v: UdlValue) => {
|
|
501
855
|
operands.push(v);
|
|
502
|
-
checkValue(v, arithmetic ? resultType : "money", where, input);
|
|
856
|
+
checkValue(v, arithmetic ? resultType : "money", where, input, act);
|
|
503
857
|
};
|
|
504
858
|
if (c.op === "at") {
|
|
505
|
-
expect(c.list, "list", where, input);
|
|
506
|
-
checkValue(c.position, "integer", where, input);
|
|
859
|
+
expect(c.list, "list", where, input, act);
|
|
860
|
+
checkValue(c.position, "integer", where, input, act);
|
|
507
861
|
if ("literal" in c.position && Number(c.position.literal) < 1)
|
|
508
862
|
add(where, "list positions start at one");
|
|
509
863
|
operands.push({ field: c.list }, c.position);
|
|
@@ -522,15 +876,15 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
522
876
|
money(c.amount);
|
|
523
877
|
const type =
|
|
524
878
|
"field" in c.numerator
|
|
525
|
-
? field(c.numerator.field, input)?.type
|
|
879
|
+
? field(c.numerator.field, input, act)?.type
|
|
526
880
|
: typeof c.numerator.literal === "number"
|
|
527
881
|
? "integer"
|
|
528
882
|
: "money";
|
|
529
883
|
if (type !== "integer" && type !== "money" && type !== "percent")
|
|
530
884
|
add(where, "ratio weights must share a numeric type");
|
|
531
885
|
else {
|
|
532
|
-
checkValue(c.numerator, type, where, input);
|
|
533
|
-
checkValue(c.denominator, type, where, input);
|
|
886
|
+
checkValue(c.numerator, type, where, input, act);
|
|
887
|
+
checkValue(c.denominator, type, where, input, act);
|
|
534
888
|
}
|
|
535
889
|
if (
|
|
536
890
|
"literal" in c.denominator &&
|
|
@@ -546,13 +900,13 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
546
900
|
}
|
|
547
901
|
if (c.op === "rate") {
|
|
548
902
|
money(c.base);
|
|
549
|
-
checkValue(c.bps, "percent", where, input);
|
|
903
|
+
checkValue(c.bps, "percent", where, input, act);
|
|
550
904
|
operands.push(c.bps);
|
|
551
905
|
}
|
|
552
906
|
if (c.op === "multiply" || c.op === "divide") {
|
|
553
907
|
money(c.amount);
|
|
554
908
|
const count = c.op === "multiply" ? c.units : c.divisor;
|
|
555
|
-
checkValue(count, "integer", where, input);
|
|
909
|
+
checkValue(count, "integer", where, input, act);
|
|
556
910
|
operands.push(count);
|
|
557
911
|
if (
|
|
558
912
|
"literal" in count &&
|
|
@@ -566,8 +920,8 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
566
920
|
);
|
|
567
921
|
}
|
|
568
922
|
if (c.op === "shift") {
|
|
569
|
-
checkValue(c.date, "date", where, input);
|
|
570
|
-
checkValue(c.milliseconds, "duration", where, input);
|
|
923
|
+
checkValue(c.date, "date", where, input, act);
|
|
924
|
+
checkValue(c.milliseconds, "duration", where, input, act);
|
|
571
925
|
operands.push(c.date, c.milliseconds);
|
|
572
926
|
}
|
|
573
927
|
dependencies.set(
|
|
@@ -628,13 +982,16 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
628
982
|
requirements: typeof inst.invariants,
|
|
629
983
|
where: string,
|
|
630
984
|
input: readonly UdlField[] = [],
|
|
985
|
+
act?: UdlAction,
|
|
631
986
|
) => {
|
|
632
987
|
for (const req of requirements ?? []) {
|
|
633
988
|
if (req.kind === "compare") {
|
|
634
989
|
const left =
|
|
635
|
-
"field" in req.left ? field(req.left.field, input) : undefined;
|
|
990
|
+
"field" in req.left ? field(req.left.field, input, act) : undefined;
|
|
636
991
|
const right =
|
|
637
|
-
"field" in req.right
|
|
992
|
+
"field" in req.right
|
|
993
|
+
? field(req.right.field, input, act)
|
|
994
|
+
: undefined;
|
|
638
995
|
if (
|
|
639
996
|
("field" in req.left && !left) ||
|
|
640
997
|
("field" in req.right && !right)
|
|
@@ -646,11 +1003,12 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
646
1003
|
(left.type !== right.type ||
|
|
647
1004
|
(left.type === "ref" &&
|
|
648
1005
|
right.type === "ref" &&
|
|
649
|
-
|
|
1006
|
+
(left.targetKind !== right.targetKind ||
|
|
1007
|
+
!overlap(left.target, right.target))))
|
|
650
1008
|
)
|
|
651
1009
|
add(where, "comparison operands have different types");
|
|
652
1010
|
if (left && "literal" in req.right) {
|
|
653
|
-
checkValue(req.right, left.type, where, input);
|
|
1011
|
+
checkValue(req.right, left.type, where, input, act);
|
|
654
1012
|
if (
|
|
655
1013
|
left.type === "enum" &&
|
|
656
1014
|
!left.values.includes(String(req.right.literal))
|
|
@@ -658,7 +1016,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
658
1016
|
add(where, "comparison names an undeclared enum value");
|
|
659
1017
|
}
|
|
660
1018
|
if (right && "literal" in req.left) {
|
|
661
|
-
checkValue(req.left, right.type, where, input);
|
|
1019
|
+
checkValue(req.left, right.type, where, input, act);
|
|
662
1020
|
if (
|
|
663
1021
|
right.type === "enum" &&
|
|
664
1022
|
!right.values.includes(String(req.left.literal))
|
|
@@ -666,9 +1024,10 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
666
1024
|
add(where, "comparison names an undeclared enum value");
|
|
667
1025
|
}
|
|
668
1026
|
} else if (req.kind === "state") {
|
|
669
|
-
const target = expect(req.reference, "ref", where, input);
|
|
1027
|
+
const target = expect(req.reference, "ref", where, input, act);
|
|
670
1028
|
if (
|
|
671
1029
|
target?.type === "ref" &&
|
|
1030
|
+
target.targetKind === "instrument" &&
|
|
672
1031
|
req.states.some((s) =>
|
|
673
1032
|
targetIds(target.target).some(
|
|
674
1033
|
(id) => !byId.get(id)?.lifecycle.states.includes(s),
|
|
@@ -678,28 +1037,59 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
678
1037
|
add(where, "reference gate names an undeclared state");
|
|
679
1038
|
} else if (req.kind === "unique") {
|
|
680
1039
|
for (const p of req.fields)
|
|
681
|
-
if (!field(p, input
|
|
1040
|
+
if (!field(p, input, act))
|
|
1041
|
+
add(where, `unknown identity field ${p}`);
|
|
682
1042
|
} else if (req.kind === "approval") {
|
|
683
|
-
const
|
|
684
|
-
|
|
685
|
-
|
|
1043
|
+
const request = expect(
|
|
1044
|
+
req.protectedRequest,
|
|
1045
|
+
"ref",
|
|
1046
|
+
where,
|
|
1047
|
+
input,
|
|
1048
|
+
act,
|
|
1049
|
+
);
|
|
1050
|
+
if (request?.type === "ref" && request.targetKind !== "instrument")
|
|
1051
|
+
add(
|
|
1052
|
+
where,
|
|
1053
|
+
"protectedRequest must reference an instrument",
|
|
1054
|
+
"UDL5001",
|
|
1055
|
+
);
|
|
1056
|
+
const target = expect(req.target, "ref", where, input, act);
|
|
1057
|
+
if (!approvalParty(document, req.party))
|
|
1058
|
+
add(
|
|
1059
|
+
where,
|
|
1060
|
+
`approval requires staff with a role: ${req.party}`,
|
|
1061
|
+
hasParty(document, inst, req.party)
|
|
1062
|
+
? "party_kind_mismatch"
|
|
1063
|
+
: "subject_party_unbound",
|
|
1064
|
+
);
|
|
686
1065
|
if (
|
|
687
1066
|
req.action &&
|
|
688
1067
|
target?.type === "ref" &&
|
|
1068
|
+
target.targetKind === "instrument" &&
|
|
689
1069
|
targetIds(target.target).some(
|
|
690
1070
|
(id) => !byId.get(id)?.actions[req.action!],
|
|
691
1071
|
)
|
|
692
1072
|
)
|
|
693
1073
|
add(where, `unknown approved action ${req.action}`);
|
|
694
1074
|
} else if (req.kind === "evidence") {
|
|
695
|
-
|
|
696
|
-
|
|
1075
|
+
if (req.instruction) {
|
|
1076
|
+
expect(req.instruction, "text", where, input, act);
|
|
1077
|
+
if (!["confirmed", "rejected"].includes(req.result))
|
|
1078
|
+
add(where, "instruction evidence requires a terminal outcome");
|
|
1079
|
+
}
|
|
1080
|
+
const subject = field(req.subject, input, act);
|
|
1081
|
+
if (
|
|
1082
|
+
!subject ||
|
|
1083
|
+
!["account", "text", ...(req.instruction ? ["ref"] : [])].includes(
|
|
1084
|
+
subject.type,
|
|
1085
|
+
)
|
|
1086
|
+
)
|
|
697
1087
|
add(
|
|
698
1088
|
where,
|
|
699
1089
|
"evidence subject must be an account or text subject id",
|
|
700
1090
|
);
|
|
701
1091
|
} else if (req.kind === "hours") {
|
|
702
|
-
expect(req.at, "date", where, input);
|
|
1092
|
+
expect(req.at, "date", where, input, act);
|
|
703
1093
|
try {
|
|
704
1094
|
new Intl.DateTimeFormat("en", { timeZone: req.timezone });
|
|
705
1095
|
} catch {
|
|
@@ -723,6 +1113,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
723
1113
|
req.measure === "count" ? "integer" : "money",
|
|
724
1114
|
where,
|
|
725
1115
|
input,
|
|
1116
|
+
act,
|
|
726
1117
|
);
|
|
727
1118
|
}
|
|
728
1119
|
}
|
|
@@ -733,11 +1124,83 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
733
1124
|
const where = `${base}.actions.${actionName}`;
|
|
734
1125
|
if (actionName !== "create" && !inst.lifecycle.transitions[actionName])
|
|
735
1126
|
add(where, "action needs a lifecycle transition", "UDL3001");
|
|
736
|
-
|
|
1127
|
+
if (action.subject) {
|
|
1128
|
+
const subBase = `${where}.subject`;
|
|
1129
|
+
checkFields(
|
|
1130
|
+
action.subject.requirements.map((r) => ({
|
|
1131
|
+
...r.field,
|
|
1132
|
+
name: r.objectField ?? r.field.name,
|
|
1133
|
+
})),
|
|
1134
|
+
`${subBase}.requirements`,
|
|
1135
|
+
true,
|
|
1136
|
+
);
|
|
1137
|
+
for (const req of action.subject.requirements) {
|
|
1138
|
+
if (req.field.optional)
|
|
1139
|
+
add(
|
|
1140
|
+
`${subBase}.requirements`,
|
|
1141
|
+
`subject requirement ${req.field.name} cannot be optional`,
|
|
1142
|
+
"UDL2002",
|
|
1143
|
+
);
|
|
1144
|
+
const name = req.objectField ?? req.field.name;
|
|
1145
|
+
if (reservedObjectNamesSet.has(name))
|
|
1146
|
+
add(subBase, `${name} is a reserved object name`);
|
|
1147
|
+
if (inst.subject) {
|
|
1148
|
+
const field = byObjectId
|
|
1149
|
+
.get(inst.subject)
|
|
1150
|
+
?.fields.find((field) => field.name === name);
|
|
1151
|
+
if (!field || !sameObjectField(field, req.field))
|
|
1152
|
+
add(
|
|
1153
|
+
subBase,
|
|
1154
|
+
`object field ${name} must match requirement ${req.field.name}`,
|
|
1155
|
+
field ? "subject_field_conflict" : "subject_field_unknown",
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
duplicate(
|
|
1160
|
+
action.subject.adapters.map((a) => a.binding),
|
|
1161
|
+
`${subBase}.adapters`,
|
|
1162
|
+
);
|
|
1163
|
+
for (const adapter of action.subject.adapters) {
|
|
1164
|
+
if (adapter.snapshot) {
|
|
1165
|
+
checkFields(
|
|
1166
|
+
adapter.snapshot.requirements,
|
|
1167
|
+
`${subBase}.adapters.${adapter.binding}.requirements`,
|
|
1168
|
+
true,
|
|
1169
|
+
);
|
|
1170
|
+
for (const reqField of adapter.snapshot.requirements) {
|
|
1171
|
+
const objectName =
|
|
1172
|
+
adapter.renames?.[reqField.name] ?? reqField.name;
|
|
1173
|
+
const requirement = action.subject.requirements.find(
|
|
1174
|
+
(item) =>
|
|
1175
|
+
(item.objectField ?? item.field.name) === objectName &&
|
|
1176
|
+
sameObjectField(item.field, reqField),
|
|
1177
|
+
);
|
|
1178
|
+
if (!requirement)
|
|
1179
|
+
add(
|
|
1180
|
+
subBase,
|
|
1181
|
+
`adapter ${adapter.binding} requirement ${reqField.name} is absent from the action`,
|
|
1182
|
+
);
|
|
1183
|
+
if (reqField.optional)
|
|
1184
|
+
add(
|
|
1185
|
+
`${subBase}.adapters.${adapter.binding}`,
|
|
1186
|
+
`adapter requirement ${reqField.name} cannot be optional`,
|
|
1187
|
+
"UDL2002",
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
checkFields(action.input, `${where}.input`, false, inst);
|
|
737
1194
|
if (action.reminder) {
|
|
738
|
-
expect(action.reminder.installment, "ref", where, action.input);
|
|
739
|
-
expect(
|
|
740
|
-
|
|
1195
|
+
expect(action.reminder.installment, "ref", where, action.input, action);
|
|
1196
|
+
expect(
|
|
1197
|
+
action.reminder.recipient,
|
|
1198
|
+
"account",
|
|
1199
|
+
where,
|
|
1200
|
+
action.input,
|
|
1201
|
+
action,
|
|
1202
|
+
);
|
|
1203
|
+
expect(action.reminder.dueAt, "date", where, action.input, action);
|
|
741
1204
|
if (action.reminder.startHour >= action.reminder.endHour)
|
|
742
1205
|
add(where, "reminder sending window is empty");
|
|
743
1206
|
try {
|
|
@@ -773,12 +1236,18 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
773
1236
|
action.calculate ?? [],
|
|
774
1237
|
`${where}.calculate`,
|
|
775
1238
|
action.input,
|
|
1239
|
+
action,
|
|
1240
|
+
);
|
|
1241
|
+
checkRequirements(
|
|
1242
|
+
action.requires,
|
|
1243
|
+
`${where}.requires`,
|
|
1244
|
+
action.input,
|
|
1245
|
+
action,
|
|
776
1246
|
);
|
|
777
|
-
checkRequirements(action.requires, `${where}.requires`, action.input);
|
|
778
1247
|
if (
|
|
779
1248
|
typeof action.actor === "object" &&
|
|
780
1249
|
"party" in action.actor &&
|
|
781
|
-
!document
|
|
1250
|
+
!hasParty(document, inst, action.actor.party)
|
|
782
1251
|
)
|
|
783
1252
|
add(where, "actor names an undeclared party");
|
|
784
1253
|
if (
|
|
@@ -791,7 +1260,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
791
1260
|
add(where, "clock action needs a due instant");
|
|
792
1261
|
for (const clock of [action.due, action.deadline])
|
|
793
1262
|
if (clock) {
|
|
794
|
-
expect(clock.at, "date", where, action.input);
|
|
1263
|
+
expect(clock.at, "date", where, action.input, action);
|
|
795
1264
|
if (clock.localDay) {
|
|
796
1265
|
try {
|
|
797
1266
|
new Intl.DateTimeFormat("en", {
|
|
@@ -803,10 +1272,28 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
803
1272
|
}
|
|
804
1273
|
}
|
|
805
1274
|
for (const move of action.moves) {
|
|
1275
|
+
if (
|
|
1276
|
+
move.operation === "internal_transfer.reserve" &&
|
|
1277
|
+
move.boundary &&
|
|
1278
|
+
!action.subject?.adapters.some(
|
|
1279
|
+
(binding) =>
|
|
1280
|
+
binding.binding === move.boundary!.adapter && binding.snapshot,
|
|
1281
|
+
)
|
|
1282
|
+
)
|
|
1283
|
+
add(
|
|
1284
|
+
where,
|
|
1285
|
+
"boundary reservation requires a retained adapter binding",
|
|
1286
|
+
);
|
|
806
1287
|
if ("amount" in move) {
|
|
807
|
-
checkValue(move.amount, "money", where, action.input);
|
|
808
|
-
const from = expect(
|
|
809
|
-
|
|
1288
|
+
checkValue(move.amount, "money", where, action.input, action);
|
|
1289
|
+
const from = expect(
|
|
1290
|
+
move.from,
|
|
1291
|
+
"account",
|
|
1292
|
+
where,
|
|
1293
|
+
action.input,
|
|
1294
|
+
action,
|
|
1295
|
+
);
|
|
1296
|
+
const to = expect(move.to, "account", where, action.input, action);
|
|
810
1297
|
if (
|
|
811
1298
|
from?.type === "account" &&
|
|
812
1299
|
to?.type === "account" &&
|
|
@@ -822,7 +1309,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
822
1309
|
(from.key ?? "balance") === (to.key ?? "balance");
|
|
823
1310
|
if (move.from === move.to || samePartyAccount)
|
|
824
1311
|
add(where, "a transfer needs distinct accounts", "UDL4001");
|
|
825
|
-
} else expect(move.transfer, "text", where, action.input);
|
|
1312
|
+
} else expect(move.transfer, "text", where, action.input, action);
|
|
826
1313
|
}
|
|
827
1314
|
duplicate(
|
|
828
1315
|
action.moves.flatMap((m) =>
|
|
@@ -845,7 +1332,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
845
1332
|
inst.calculate.some((c) => c.target === key)
|
|
846
1333
|
)
|
|
847
1334
|
add(where, `cannot write immutable field ${key}`);
|
|
848
|
-
else checkValue(v, target.type, where, action.input);
|
|
1335
|
+
else checkValue(v, target.type, where, action.input, action);
|
|
849
1336
|
}
|
|
850
1337
|
const checkArguments = (
|
|
851
1338
|
targetId: string | undefined,
|
|
@@ -875,11 +1362,12 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
875
1362
|
add(where, "range binding requires an integer target input");
|
|
876
1363
|
continue;
|
|
877
1364
|
}
|
|
878
|
-
checkValue(value, declared.type, where, action.input);
|
|
1365
|
+
checkValue(value, declared.type, where, action.input, action);
|
|
879
1366
|
if (declared.type === "ref" && "field" in value) {
|
|
880
|
-
const supplied = field(value.field, action.input);
|
|
1367
|
+
const supplied = field(value.field, action.input, action);
|
|
881
1368
|
if (
|
|
882
1369
|
supplied?.type !== "ref" ||
|
|
1370
|
+
supplied.targetKind !== declared.targetKind ||
|
|
883
1371
|
!targetIds(supplied.target).every((id) =>
|
|
884
1372
|
targetIds(declared.target).includes(id),
|
|
885
1373
|
)
|
|
@@ -889,33 +1377,55 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
889
1377
|
}
|
|
890
1378
|
};
|
|
891
1379
|
if (action.approval) {
|
|
1380
|
+
const request = expect(
|
|
1381
|
+
action.approval.protectedRequest,
|
|
1382
|
+
"ref",
|
|
1383
|
+
where,
|
|
1384
|
+
action.input,
|
|
1385
|
+
action,
|
|
1386
|
+
);
|
|
1387
|
+
if (request?.type === "ref" && request.targetKind !== "instrument")
|
|
1388
|
+
add(
|
|
1389
|
+
where,
|
|
1390
|
+
"protectedRequest must reference an instrument",
|
|
1391
|
+
"UDL5001",
|
|
1392
|
+
);
|
|
892
1393
|
const reference = expect(
|
|
893
1394
|
action.approval.target,
|
|
894
1395
|
"ref",
|
|
895
1396
|
where,
|
|
896
1397
|
action.input,
|
|
1398
|
+
action,
|
|
897
1399
|
);
|
|
898
1400
|
if (
|
|
899
1401
|
reference?.type === "ref" &&
|
|
1402
|
+
reference.targetKind === "instrument" &&
|
|
900
1403
|
targetIds(reference.target).some(
|
|
901
1404
|
(id) => !byId.get(id)?.actions[action.approval!.action],
|
|
902
1405
|
)
|
|
903
1406
|
)
|
|
904
1407
|
add(where, "approval target action does not exist");
|
|
905
|
-
if (reference?.type === "ref")
|
|
1408
|
+
if (reference?.type === "ref" && reference.targetKind === "instrument")
|
|
906
1409
|
for (const id of targetIds(reference.target))
|
|
907
1410
|
checkArguments(id, action.approval.action, action.approval.input);
|
|
908
|
-
expect(action.approval.expires, "date", where, action.input);
|
|
909
|
-
if (!document
|
|
910
|
-
add(
|
|
1411
|
+
expect(action.approval.expires, "date", where, action.input, action);
|
|
1412
|
+
if (!approvalParty(document, action.approval.party))
|
|
1413
|
+
add(
|
|
1414
|
+
where,
|
|
1415
|
+
"approval requires staff with a role",
|
|
1416
|
+
hasParty(document, inst, action.approval.party)
|
|
1417
|
+
? "party_kind_mismatch"
|
|
1418
|
+
: "subject_party_unbound",
|
|
1419
|
+
);
|
|
911
1420
|
}
|
|
912
1421
|
const targets: { targets: string[]; count: number }[] = [];
|
|
913
1422
|
for (const call of action.invoke ?? []) {
|
|
914
|
-
if (call.guard)
|
|
1423
|
+
if (call.guard)
|
|
1424
|
+
checkRequirements([call.guard], where, action.input, action);
|
|
915
1425
|
const range = "instrument" in call ? call.range : undefined;
|
|
916
1426
|
if (range) {
|
|
917
|
-
checkValue(range.count, "integer", where, action.input);
|
|
918
|
-
if (["self", "input", "party"].includes(range.bind))
|
|
1427
|
+
checkValue(range.count, "integer", where, action.input, action);
|
|
1428
|
+
if (["self", "input", "party", "subject"].includes(range.bind))
|
|
919
1429
|
add(where, "range binding cannot shadow a reserved scope");
|
|
920
1430
|
if (
|
|
921
1431
|
"literal" in range.count &&
|
|
@@ -928,13 +1438,15 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
928
1438
|
if ("selection" in call)
|
|
929
1439
|
checkSelection(call.selection, where, action.input);
|
|
930
1440
|
const target =
|
|
931
|
-
"reference" in call
|
|
1441
|
+
"reference" in call
|
|
1442
|
+
? field(call.reference, action.input, action)
|
|
1443
|
+
: undefined;
|
|
932
1444
|
const selected =
|
|
933
1445
|
"instrument" in call
|
|
934
1446
|
? call.instrument
|
|
935
1447
|
: "selection" in call
|
|
936
1448
|
? call.selection.instrument
|
|
937
|
-
: target?.type === "ref"
|
|
1449
|
+
: target?.type === "ref" && target.targetKind === "instrument"
|
|
938
1450
|
? target.target
|
|
939
1451
|
: undefined;
|
|
940
1452
|
const ids = Array.isArray(selected)
|
|
@@ -950,8 +1462,35 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
950
1462
|
where,
|
|
951
1463
|
"invocation references an existing instance and cannot create it again",
|
|
952
1464
|
);
|
|
953
|
-
|
|
1465
|
+
const targetInst = byId.get(targetId);
|
|
1466
|
+
if (!targetInst?.actions[call.action])
|
|
954
1467
|
add(where, "invocation target must name a declared action");
|
|
1468
|
+
else if (!call.guard) {
|
|
1469
|
+
const targetAction = targetInst.actions[call.action]!;
|
|
1470
|
+
if (
|
|
1471
|
+
inst.subject &&
|
|
1472
|
+
targetInst.subject &&
|
|
1473
|
+
inst.subject === targetInst.subject
|
|
1474
|
+
) {
|
|
1475
|
+
const callerRequirements = action.subject?.requirements ?? [];
|
|
1476
|
+
for (const targetReq of targetAction.subject?.requirements ??
|
|
1477
|
+
[]) {
|
|
1478
|
+
const targetObjFieldName =
|
|
1479
|
+
targetReq.objectField ?? targetReq.field.name;
|
|
1480
|
+
const satisfied = callerRequirements.some(
|
|
1481
|
+
(cr) =>
|
|
1482
|
+
(cr.objectField ?? cr.field.name) === targetObjFieldName,
|
|
1483
|
+
);
|
|
1484
|
+
if (!satisfied) {
|
|
1485
|
+
add(
|
|
1486
|
+
`${where}.invoke`,
|
|
1487
|
+
`mandatory invocation of ${targetId}.${call.action} requires subject field ${targetReq.field.name}`,
|
|
1488
|
+
"UDL2002",
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
955
1494
|
}
|
|
956
1495
|
targets.push({
|
|
957
1496
|
targets: ids.map((id) => `${id}.${call.action}`),
|