@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/src/validation.ts
CHANGED
|
@@ -1,15 +1,71 @@
|
|
|
1
1
|
import {
|
|
2
2
|
udlDocumentSchema,
|
|
3
|
+
subjectPartyRoles,
|
|
4
|
+
sameObjectField,
|
|
5
|
+
MAX_ACTION_EXPANSION,
|
|
3
6
|
type UdlDocument,
|
|
4
7
|
type UdlField,
|
|
5
8
|
type UdlInstrument,
|
|
6
9
|
type UdlValue,
|
|
7
10
|
type UdlCalculation,
|
|
8
11
|
type UdlSelection,
|
|
12
|
+
type UdlAction,
|
|
13
|
+
type UdlActionSubject,
|
|
14
|
+
type UdlSubjectRequirement,
|
|
15
|
+
type UdlObjectField,
|
|
16
|
+
type UdlObjectKind,
|
|
9
17
|
} from "./schema.js";
|
|
10
18
|
import { issue, type UdlIssue } from "./diagnostics.js";
|
|
19
|
+
import { validateReportDefinition } from "./reporting-validation.js";
|
|
11
20
|
import { analyzeInstrumentFinance } from "./finance.js";
|
|
12
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
|
+
|
|
13
69
|
export type UdlValidationResult =
|
|
14
70
|
| { ok: true; value: UdlDocument }
|
|
15
71
|
| { ok: false; issues: readonly UdlIssue[] };
|
|
@@ -56,7 +112,9 @@ const overlap = (a: string | string[], b: string | string[]) =>
|
|
|
56
112
|
targetIds(a).some((id) => targetIds(b).includes(id));
|
|
57
113
|
|
|
58
114
|
/** Union paths retain only fields with compatible types on every member. */
|
|
59
|
-
function commonField(
|
|
115
|
+
function commonField(
|
|
116
|
+
fields: (UdlField | UdlObjectField | undefined)[],
|
|
117
|
+
): UdlField | undefined {
|
|
60
118
|
const first = fields[0];
|
|
61
119
|
if (
|
|
62
120
|
!first ||
|
|
@@ -69,6 +127,16 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
|
|
|
69
127
|
)
|
|
70
128
|
return;
|
|
71
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;
|
|
72
140
|
const targets = [
|
|
73
141
|
...new Set(
|
|
74
142
|
fields.flatMap((field) =>
|
|
@@ -76,7 +144,12 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
|
|
|
76
144
|
),
|
|
77
145
|
),
|
|
78
146
|
];
|
|
79
|
-
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;
|
|
80
153
|
}
|
|
81
154
|
if (
|
|
82
155
|
first.type === "account" &&
|
|
@@ -86,7 +159,6 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
|
|
|
86
159
|
field.book !== first.book ||
|
|
87
160
|
field.owner !== first.owner ||
|
|
88
161
|
field.key !== first.key ||
|
|
89
|
-
field.external !== first.external ||
|
|
90
162
|
field.contra !== first.contra,
|
|
91
163
|
)
|
|
92
164
|
)
|
|
@@ -107,11 +179,12 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
|
|
|
107
179
|
(field) =>
|
|
108
180
|
field?.type !== "list" ||
|
|
109
181
|
field.item !== first.item ||
|
|
182
|
+
field.targetKind !== first.targetKind ||
|
|
110
183
|
field.target !== first.target,
|
|
111
184
|
)
|
|
112
185
|
)
|
|
113
186
|
return;
|
|
114
|
-
return first;
|
|
187
|
+
return first as UdlField;
|
|
115
188
|
}
|
|
116
189
|
|
|
117
190
|
export function resolveField(
|
|
@@ -119,8 +192,71 @@ export function resolveField(
|
|
|
119
192
|
instrument: UdlInstrument,
|
|
120
193
|
path: string,
|
|
121
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>,
|
|
122
221
|
): UdlField | undefined {
|
|
123
|
-
return
|
|
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>,
|
|
251
|
+
): UdlField | undefined {
|
|
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);
|
|
124
260
|
}
|
|
125
261
|
|
|
126
262
|
function resolvePath(
|
|
@@ -129,29 +265,73 @@ function resolvePath(
|
|
|
129
265
|
path: string,
|
|
130
266
|
input: readonly UdlField[],
|
|
131
267
|
cache: Map<string, UdlField | undefined>,
|
|
268
|
+
action?: UdlAction | UdlActionSubject,
|
|
132
269
|
): UdlField | undefined {
|
|
133
270
|
const parts = path.split(".");
|
|
134
271
|
const root = parts.shift();
|
|
135
272
|
const key = parts.shift();
|
|
136
273
|
if (!key)
|
|
137
274
|
return root === "self"
|
|
138
|
-
? {
|
|
275
|
+
? {
|
|
276
|
+
name: "id",
|
|
277
|
+
type: "ref",
|
|
278
|
+
targetKind: "instrument",
|
|
279
|
+
target: instrument.id,
|
|
280
|
+
}
|
|
139
281
|
: undefined;
|
|
140
282
|
if (root === "party") {
|
|
141
|
-
if (!document
|
|
283
|
+
if (!moneyParty(document, instrument, key)) return;
|
|
142
284
|
if (parts.length === 0)
|
|
143
285
|
return { name: key, type: "account", owner: key, book: "cash" };
|
|
144
286
|
if (parts.length === 1 && ["balance", "reserved"].includes(parts[0]!))
|
|
145
287
|
return { name: parts[0]!, type: "money" };
|
|
146
288
|
return;
|
|
147
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
|
+
}
|
|
148
306
|
if (root === "self" && parts.length === 0) {
|
|
149
|
-
if (key === "id")
|
|
307
|
+
if (key === "id")
|
|
308
|
+
return {
|
|
309
|
+
name: key,
|
|
310
|
+
type: "ref",
|
|
311
|
+
targetKind: "instrument",
|
|
312
|
+
target: instrument.id,
|
|
313
|
+
};
|
|
150
314
|
if (key === "now" || key === "createdAt")
|
|
151
315
|
return { name: key, type: "date" };
|
|
316
|
+
if (key === "productRevision") return { name: key, type: "text" };
|
|
152
317
|
if (key === "status")
|
|
153
318
|
return { name: key, type: "enum", values: instrument.lifecycle.states };
|
|
154
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
|
+
|
|
155
335
|
const scope = instrument;
|
|
156
336
|
let field = (
|
|
157
337
|
root === "self" ? instrument.fields : root === "input" ? input : []
|
|
@@ -177,18 +357,11 @@ function resolvePath(
|
|
|
177
357
|
values: ["reserved", "posted", "settled", "reversed", "voided"],
|
|
178
358
|
};
|
|
179
359
|
if (field?.type !== "ref") return;
|
|
180
|
-
return
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (!target) return;
|
|
186
|
-
const suffix = "self." + parts.slice(index).join(".");
|
|
187
|
-
const key = `${id}:${suffix}`;
|
|
188
|
-
if (!cache.has(key))
|
|
189
|
-
cache.set(key, resolvePath(document, target, suffix, [], cache));
|
|
190
|
-
return cache.get(key);
|
|
191
|
-
}),
|
|
360
|
+
return resolveReferencePath(
|
|
361
|
+
document,
|
|
362
|
+
field,
|
|
363
|
+
parts.slice(index).join("."),
|
|
364
|
+
cache,
|
|
192
365
|
);
|
|
193
366
|
}
|
|
194
367
|
return field;
|
|
@@ -235,23 +408,282 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
235
408
|
seen.add(v);
|
|
236
409
|
}
|
|
237
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
|
+
);
|
|
238
418
|
duplicate(
|
|
239
419
|
document.instruments.map((i) => i.id),
|
|
240
420
|
"$.instruments",
|
|
241
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]));
|
|
242
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
|
+
|
|
642
|
+
const reportIds = new Set<string>();
|
|
643
|
+
for (const [index, instrument] of document.instruments.entries()) {
|
|
644
|
+
for (const report of instrument.reports ?? []) {
|
|
645
|
+
const path = `$.instruments[${index}].reports`;
|
|
646
|
+
if (reportIds.has(report.identity.id))
|
|
647
|
+
add(path, `duplicate report ${report.identity.id}`);
|
|
648
|
+
reportIds.add(report.identity.id);
|
|
649
|
+
try {
|
|
650
|
+
validateReportDefinition(report, document);
|
|
651
|
+
} catch (error) {
|
|
652
|
+
add(
|
|
653
|
+
path,
|
|
654
|
+
error instanceof Error ? error.message : "Invalid report definition",
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
243
660
|
const calls = new Map<string, { targets: string[]; count: number }[]>();
|
|
244
661
|
for (const [index, inst] of document.instruments.entries()) {
|
|
245
662
|
const base = `$.instruments[${index}]`;
|
|
246
|
-
|
|
247
|
-
|
|
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);
|
|
248
670
|
const expect = (
|
|
249
671
|
p: string,
|
|
250
672
|
type: UdlField["type"],
|
|
251
673
|
where: string,
|
|
252
674
|
input: readonly UdlField[] = [],
|
|
675
|
+
action?: UdlAction | UdlActionSubject,
|
|
253
676
|
) => {
|
|
254
|
-
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);
|
|
255
687
|
if (found?.type !== type)
|
|
256
688
|
add(where, `${p} must name a ${type} field`, "UDL5001");
|
|
257
689
|
return found;
|
|
@@ -261,9 +693,10 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
261
693
|
type: UdlField["type"],
|
|
262
694
|
where: string,
|
|
263
695
|
input: readonly UdlField[] = [],
|
|
696
|
+
action?: UdlAction | UdlActionSubject,
|
|
264
697
|
) => {
|
|
265
698
|
if ("field" in v) {
|
|
266
|
-
expect(v.field, type, where, input);
|
|
699
|
+
expect(v.field, type, where, input, action);
|
|
267
700
|
return;
|
|
268
701
|
}
|
|
269
702
|
const valid =
|
|
@@ -279,67 +712,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
279
712
|
: typeof v.literal === "string";
|
|
280
713
|
if (!valid) add(where, `literal must have type ${type}`);
|
|
281
714
|
};
|
|
282
|
-
|
|
283
|
-
duplicate(
|
|
284
|
-
fields.map((f) => f.name),
|
|
285
|
-
where,
|
|
286
|
-
);
|
|
287
|
-
for (const f of fields) {
|
|
288
|
-
if (
|
|
289
|
-
f.type === "account" &&
|
|
290
|
-
((f.contra && f.book !== "claim") ||
|
|
291
|
-
(f.external && (f.book !== "cash" || f.owner === "self")))
|
|
292
|
-
)
|
|
293
|
-
add(
|
|
294
|
-
where,
|
|
295
|
-
"contra accounts require the claim book; external accounts require a cash party owner",
|
|
296
|
-
);
|
|
297
|
-
if (["id", "status", "createdAt", "now"].includes(f.name))
|
|
298
|
-
add(where, `${f.name} is a sealed instance field`);
|
|
299
|
-
if (f.type === "ref") {
|
|
300
|
-
duplicate(targetIds(f.target), where);
|
|
301
|
-
for (const id of targetIds(f.target))
|
|
302
|
-
if (!byId.has(id))
|
|
303
|
-
add(where, `unknown reference target ${id}`, "UDL5001");
|
|
304
|
-
}
|
|
305
|
-
if (
|
|
306
|
-
f.type === "account" &&
|
|
307
|
-
f.owner !== "self" &&
|
|
308
|
-
!document.parties[f.owner]
|
|
309
|
-
)
|
|
310
|
-
add(where, `${f.name} needs self or a declared party as owner`);
|
|
311
|
-
if (f.type === "enum") {
|
|
312
|
-
duplicate(f.values, where);
|
|
313
|
-
if (f.value !== undefined && !f.values.includes(f.value))
|
|
314
|
-
add(where, `${f.name} constant is outside its enum`);
|
|
315
|
-
}
|
|
316
|
-
if (
|
|
317
|
-
f.type === "list" &&
|
|
318
|
-
(f.item === "ref"
|
|
319
|
-
? !f.target || !byId.has(f.target)
|
|
320
|
-
: f.target !== undefined)
|
|
321
|
-
)
|
|
322
|
-
add(
|
|
323
|
-
where,
|
|
324
|
-
`${f.name} needs a target exactly when its items are references`,
|
|
325
|
-
);
|
|
326
|
-
if (
|
|
327
|
-
(f.type === "integer" || f.type === "money") &&
|
|
328
|
-
f.minimum !== undefined &&
|
|
329
|
-
f.maximum !== undefined &&
|
|
330
|
-
BigInt(f.minimum) > BigInt(f.maximum)
|
|
331
|
-
)
|
|
332
|
-
add(where, `${f.name} minimum exceeds maximum`);
|
|
333
|
-
if (
|
|
334
|
-
(f.type === "integer" || f.type === "money") &&
|
|
335
|
-
f.value !== undefined &&
|
|
336
|
-
((f.minimum !== undefined && BigInt(f.value) < BigInt(f.minimum)) ||
|
|
337
|
-
(f.maximum !== undefined && BigInt(f.value) > BigInt(f.maximum)))
|
|
338
|
-
)
|
|
339
|
-
add(where, `${f.name} constant violates its bounds`);
|
|
340
|
-
}
|
|
341
|
-
};
|
|
342
|
-
checkFields(inst.fields, `${base}.fields`);
|
|
715
|
+
checkFields(inst.fields, `${base}.fields`, false, inst);
|
|
343
716
|
const captures = new Set(
|
|
344
717
|
Object.values(inst.actions).flatMap((a) => [
|
|
345
718
|
...a.moves.flatMap((m) =>
|
|
@@ -374,9 +747,24 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
374
747
|
a &&
|
|
375
748
|
b &&
|
|
376
749
|
a.type === b.type &&
|
|
377
|
-
(a.type !== "ref" ||
|
|
750
|
+
(a.type !== "ref" ||
|
|
751
|
+
(b.type === "ref" &&
|
|
752
|
+
a.targetKind === b.targetKind &&
|
|
753
|
+
overlap(a.target, b.target)));
|
|
378
754
|
const anchor = field(selection.anchor, input);
|
|
379
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
|
+
);
|
|
380
768
|
if (
|
|
381
769
|
selection.window &&
|
|
382
770
|
target.fields.find((f) => f.name === selection.window!.field)
|
|
@@ -436,6 +824,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
436
824
|
calculations: readonly UdlCalculation[],
|
|
437
825
|
where: string,
|
|
438
826
|
input: readonly UdlField[] = [],
|
|
827
|
+
act?: UdlAction,
|
|
439
828
|
) => {
|
|
440
829
|
duplicate(
|
|
441
830
|
calculations.map((c) => c.target),
|
|
@@ -449,7 +838,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
449
838
|
const resultType =
|
|
450
839
|
c.op === "at"
|
|
451
840
|
? (() => {
|
|
452
|
-
const list = field(c.list, input);
|
|
841
|
+
const list = field(c.list, input, act);
|
|
453
842
|
return list?.type === "list" ? list.item : "text";
|
|
454
843
|
})()
|
|
455
844
|
: (c.op === "aggregate" && c.measure === "count") ||
|
|
@@ -464,11 +853,11 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
464
853
|
const operands: UdlValue[] = [];
|
|
465
854
|
const money = (v: UdlValue) => {
|
|
466
855
|
operands.push(v);
|
|
467
|
-
checkValue(v, arithmetic ? resultType : "money", where, input);
|
|
856
|
+
checkValue(v, arithmetic ? resultType : "money", where, input, act);
|
|
468
857
|
};
|
|
469
858
|
if (c.op === "at") {
|
|
470
|
-
expect(c.list, "list", where, input);
|
|
471
|
-
checkValue(c.position, "integer", where, input);
|
|
859
|
+
expect(c.list, "list", where, input, act);
|
|
860
|
+
checkValue(c.position, "integer", where, input, act);
|
|
472
861
|
if ("literal" in c.position && Number(c.position.literal) < 1)
|
|
473
862
|
add(where, "list positions start at one");
|
|
474
863
|
operands.push({ field: c.list }, c.position);
|
|
@@ -487,15 +876,15 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
487
876
|
money(c.amount);
|
|
488
877
|
const type =
|
|
489
878
|
"field" in c.numerator
|
|
490
|
-
? field(c.numerator.field, input)?.type
|
|
879
|
+
? field(c.numerator.field, input, act)?.type
|
|
491
880
|
: typeof c.numerator.literal === "number"
|
|
492
881
|
? "integer"
|
|
493
882
|
: "money";
|
|
494
883
|
if (type !== "integer" && type !== "money" && type !== "percent")
|
|
495
884
|
add(where, "ratio weights must share a numeric type");
|
|
496
885
|
else {
|
|
497
|
-
checkValue(c.numerator, type, where, input);
|
|
498
|
-
checkValue(c.denominator, type, where, input);
|
|
886
|
+
checkValue(c.numerator, type, where, input, act);
|
|
887
|
+
checkValue(c.denominator, type, where, input, act);
|
|
499
888
|
}
|
|
500
889
|
if (
|
|
501
890
|
"literal" in c.denominator &&
|
|
@@ -511,13 +900,13 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
511
900
|
}
|
|
512
901
|
if (c.op === "rate") {
|
|
513
902
|
money(c.base);
|
|
514
|
-
checkValue(c.bps, "percent", where, input);
|
|
903
|
+
checkValue(c.bps, "percent", where, input, act);
|
|
515
904
|
operands.push(c.bps);
|
|
516
905
|
}
|
|
517
906
|
if (c.op === "multiply" || c.op === "divide") {
|
|
518
907
|
money(c.amount);
|
|
519
908
|
const count = c.op === "multiply" ? c.units : c.divisor;
|
|
520
|
-
checkValue(count, "integer", where, input);
|
|
909
|
+
checkValue(count, "integer", where, input, act);
|
|
521
910
|
operands.push(count);
|
|
522
911
|
if (
|
|
523
912
|
"literal" in count &&
|
|
@@ -531,8 +920,8 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
531
920
|
);
|
|
532
921
|
}
|
|
533
922
|
if (c.op === "shift") {
|
|
534
|
-
checkValue(c.date, "date", where, input);
|
|
535
|
-
checkValue(c.milliseconds, "duration", where, input);
|
|
923
|
+
checkValue(c.date, "date", where, input, act);
|
|
924
|
+
checkValue(c.milliseconds, "duration", where, input, act);
|
|
536
925
|
operands.push(c.date, c.milliseconds);
|
|
537
926
|
}
|
|
538
927
|
dependencies.set(
|
|
@@ -561,6 +950,8 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
561
950
|
};
|
|
562
951
|
checkCalculations(inst.calculate, `${base}.calculate`);
|
|
563
952
|
const states = new Set(inst.lifecycle.states);
|
|
953
|
+
if (states.has("preserve"))
|
|
954
|
+
add(base, "preserve is a reserved transition destination", "UDL3001");
|
|
564
955
|
duplicate(inst.lifecycle.states, `${base}.lifecycle.states`);
|
|
565
956
|
if (!states.has(inst.lifecycle.initial) || !inst.actions.create)
|
|
566
957
|
add(base, "declare create and a declared initial state", "UDL3001");
|
|
@@ -575,14 +966,15 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
575
966
|
if (
|
|
576
967
|
!inst.actions[action] ||
|
|
577
968
|
action === "create" ||
|
|
578
|
-
!states.has(edge.to) ||
|
|
969
|
+
(edge.to !== "preserve" && !states.has(edge.to)) ||
|
|
579
970
|
edge.from.some((s) => !states.has(s))
|
|
580
971
|
)
|
|
581
972
|
add(base, `invalid transition ${action}`, "UDL3001");
|
|
582
973
|
}
|
|
583
974
|
for (let pass = 0; pass < states.size; pass++)
|
|
584
975
|
for (const edge of Object.values(inst.lifecycle.transitions))
|
|
585
|
-
if (edge.from.some((s) => reachable.has(s)))
|
|
976
|
+
if (edge.to !== "preserve" && edge.from.some((s) => reachable.has(s)))
|
|
977
|
+
reachable.add(edge.to);
|
|
586
978
|
for (const state of states)
|
|
587
979
|
if (!reachable.has(state))
|
|
588
980
|
add(base, `unreachable state ${state}`, "UDL3001");
|
|
@@ -590,13 +982,16 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
590
982
|
requirements: typeof inst.invariants,
|
|
591
983
|
where: string,
|
|
592
984
|
input: readonly UdlField[] = [],
|
|
985
|
+
act?: UdlAction,
|
|
593
986
|
) => {
|
|
594
987
|
for (const req of requirements ?? []) {
|
|
595
988
|
if (req.kind === "compare") {
|
|
596
989
|
const left =
|
|
597
|
-
"field" in req.left ? field(req.left.field, input) : undefined;
|
|
990
|
+
"field" in req.left ? field(req.left.field, input, act) : undefined;
|
|
598
991
|
const right =
|
|
599
|
-
"field" in req.right
|
|
992
|
+
"field" in req.right
|
|
993
|
+
? field(req.right.field, input, act)
|
|
994
|
+
: undefined;
|
|
600
995
|
if (
|
|
601
996
|
("field" in req.left && !left) ||
|
|
602
997
|
("field" in req.right && !right)
|
|
@@ -608,11 +1003,12 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
608
1003
|
(left.type !== right.type ||
|
|
609
1004
|
(left.type === "ref" &&
|
|
610
1005
|
right.type === "ref" &&
|
|
611
|
-
|
|
1006
|
+
(left.targetKind !== right.targetKind ||
|
|
1007
|
+
!overlap(left.target, right.target))))
|
|
612
1008
|
)
|
|
613
1009
|
add(where, "comparison operands have different types");
|
|
614
1010
|
if (left && "literal" in req.right) {
|
|
615
|
-
checkValue(req.right, left.type, where, input);
|
|
1011
|
+
checkValue(req.right, left.type, where, input, act);
|
|
616
1012
|
if (
|
|
617
1013
|
left.type === "enum" &&
|
|
618
1014
|
!left.values.includes(String(req.right.literal))
|
|
@@ -620,7 +1016,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
620
1016
|
add(where, "comparison names an undeclared enum value");
|
|
621
1017
|
}
|
|
622
1018
|
if (right && "literal" in req.left) {
|
|
623
|
-
checkValue(req.left, right.type, where, input);
|
|
1019
|
+
checkValue(req.left, right.type, where, input, act);
|
|
624
1020
|
if (
|
|
625
1021
|
right.type === "enum" &&
|
|
626
1022
|
!right.values.includes(String(req.left.literal))
|
|
@@ -628,9 +1024,10 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
628
1024
|
add(where, "comparison names an undeclared enum value");
|
|
629
1025
|
}
|
|
630
1026
|
} else if (req.kind === "state") {
|
|
631
|
-
const target = expect(req.reference, "ref", where, input);
|
|
1027
|
+
const target = expect(req.reference, "ref", where, input, act);
|
|
632
1028
|
if (
|
|
633
1029
|
target?.type === "ref" &&
|
|
1030
|
+
target.targetKind === "instrument" &&
|
|
634
1031
|
req.states.some((s) =>
|
|
635
1032
|
targetIds(target.target).some(
|
|
636
1033
|
(id) => !byId.get(id)?.lifecycle.states.includes(s),
|
|
@@ -640,28 +1037,59 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
640
1037
|
add(where, "reference gate names an undeclared state");
|
|
641
1038
|
} else if (req.kind === "unique") {
|
|
642
1039
|
for (const p of req.fields)
|
|
643
|
-
if (!field(p, input
|
|
1040
|
+
if (!field(p, input, act))
|
|
1041
|
+
add(where, `unknown identity field ${p}`);
|
|
644
1042
|
} else if (req.kind === "approval") {
|
|
645
|
-
const
|
|
646
|
-
|
|
647
|
-
|
|
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
|
+
);
|
|
648
1065
|
if (
|
|
649
1066
|
req.action &&
|
|
650
1067
|
target?.type === "ref" &&
|
|
1068
|
+
target.targetKind === "instrument" &&
|
|
651
1069
|
targetIds(target.target).some(
|
|
652
1070
|
(id) => !byId.get(id)?.actions[req.action!],
|
|
653
1071
|
)
|
|
654
1072
|
)
|
|
655
1073
|
add(where, `unknown approved action ${req.action}`);
|
|
656
1074
|
} else if (req.kind === "evidence") {
|
|
657
|
-
|
|
658
|
-
|
|
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
|
+
)
|
|
659
1087
|
add(
|
|
660
1088
|
where,
|
|
661
1089
|
"evidence subject must be an account or text subject id",
|
|
662
1090
|
);
|
|
663
1091
|
} else if (req.kind === "hours") {
|
|
664
|
-
expect(req.at, "date", where, input);
|
|
1092
|
+
expect(req.at, "date", where, input, act);
|
|
665
1093
|
try {
|
|
666
1094
|
new Intl.DateTimeFormat("en", { timeZone: req.timezone });
|
|
667
1095
|
} catch {
|
|
@@ -685,6 +1113,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
685
1113
|
req.measure === "count" ? "integer" : "money",
|
|
686
1114
|
where,
|
|
687
1115
|
input,
|
|
1116
|
+
act,
|
|
688
1117
|
);
|
|
689
1118
|
}
|
|
690
1119
|
}
|
|
@@ -695,7 +1124,91 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
695
1124
|
const where = `${base}.actions.${actionName}`;
|
|
696
1125
|
if (actionName !== "create" && !inst.lifecycle.transitions[actionName])
|
|
697
1126
|
add(where, "action needs a lifecycle transition", "UDL3001");
|
|
698
|
-
|
|
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);
|
|
1194
|
+
if (action.reminder) {
|
|
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);
|
|
1204
|
+
if (action.reminder.startHour >= action.reminder.endHour)
|
|
1205
|
+
add(where, "reminder sending window is empty");
|
|
1206
|
+
try {
|
|
1207
|
+
new Intl.DateTimeFormat("en", { timeZone: action.reminder.timezone });
|
|
1208
|
+
} catch {
|
|
1209
|
+
add(where, "reminder requires an IANA timezone");
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
699
1212
|
for (const input of action.input) {
|
|
700
1213
|
if (input.type === "account")
|
|
701
1214
|
add(where, "account bindings cannot be caller inputs");
|
|
@@ -723,29 +1236,64 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
723
1236
|
action.calculate ?? [],
|
|
724
1237
|
`${where}.calculate`,
|
|
725
1238
|
action.input,
|
|
1239
|
+
action,
|
|
1240
|
+
);
|
|
1241
|
+
checkRequirements(
|
|
1242
|
+
action.requires,
|
|
1243
|
+
`${where}.requires`,
|
|
1244
|
+
action.input,
|
|
1245
|
+
action,
|
|
726
1246
|
);
|
|
727
|
-
checkRequirements(action.requires, `${where}.requires`, action.input);
|
|
728
1247
|
if (
|
|
729
1248
|
typeof action.actor === "object" &&
|
|
730
1249
|
"party" in action.actor &&
|
|
731
|
-
!document
|
|
1250
|
+
!hasParty(document, inst, action.actor.party)
|
|
732
1251
|
)
|
|
733
1252
|
add(where, "actor names an undeclared party");
|
|
734
1253
|
if (
|
|
735
1254
|
typeof action.actor === "object" &&
|
|
736
1255
|
"parent" in action.actor &&
|
|
737
|
-
|
|
1256
|
+
targetIds(action.actor.parent).some((id) => !byId.has(id))
|
|
738
1257
|
)
|
|
739
1258
|
add(where, "actor names an undeclared parent");
|
|
740
1259
|
if (action.actor === "clock" && !action.due)
|
|
741
1260
|
add(where, "clock action needs a due instant");
|
|
742
1261
|
for (const clock of [action.due, action.deadline])
|
|
743
|
-
if (clock)
|
|
1262
|
+
if (clock) {
|
|
1263
|
+
expect(clock.at, "date", where, action.input, action);
|
|
1264
|
+
if (clock.localDay) {
|
|
1265
|
+
try {
|
|
1266
|
+
new Intl.DateTimeFormat("en", {
|
|
1267
|
+
timeZone: clock.localDay.timezone,
|
|
1268
|
+
}).format(0);
|
|
1269
|
+
} catch {
|
|
1270
|
+
add(where, "clock timezone must be an IANA timezone");
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
744
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
|
+
);
|
|
745
1287
|
if ("amount" in move) {
|
|
746
|
-
checkValue(move.amount, "money", where, action.input);
|
|
747
|
-
const from = expect(
|
|
748
|
-
|
|
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);
|
|
749
1297
|
if (
|
|
750
1298
|
from?.type === "account" &&
|
|
751
1299
|
to?.type === "account" &&
|
|
@@ -761,7 +1309,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
761
1309
|
(from.key ?? "balance") === (to.key ?? "balance");
|
|
762
1310
|
if (move.from === move.to || samePartyAccount)
|
|
763
1311
|
add(where, "a transfer needs distinct accounts", "UDL4001");
|
|
764
|
-
} else expect(move.transfer, "text", where, action.input);
|
|
1312
|
+
} else expect(move.transfer, "text", where, action.input, action);
|
|
765
1313
|
}
|
|
766
1314
|
duplicate(
|
|
767
1315
|
action.moves.flatMap((m) =>
|
|
@@ -784,12 +1332,13 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
784
1332
|
inst.calculate.some((c) => c.target === key)
|
|
785
1333
|
)
|
|
786
1334
|
add(where, `cannot write immutable field ${key}`);
|
|
787
|
-
else checkValue(v, target.type, where, action.input);
|
|
1335
|
+
else checkValue(v, target.type, where, action.input, action);
|
|
788
1336
|
}
|
|
789
1337
|
const checkArguments = (
|
|
790
1338
|
targetId: string | undefined,
|
|
791
1339
|
actionName: string,
|
|
792
1340
|
values: Record<string, UdlValue>,
|
|
1341
|
+
binding?: string,
|
|
793
1342
|
) => {
|
|
794
1343
|
const target = targetId
|
|
795
1344
|
? byId.get(targetId)?.actions[actionName]
|
|
@@ -808,11 +1357,17 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
808
1357
|
add(where, `unknown target input ${name}`);
|
|
809
1358
|
continue;
|
|
810
1359
|
}
|
|
811
|
-
|
|
1360
|
+
if (binding && "field" in value && value.field === binding) {
|
|
1361
|
+
if (declared.type !== "integer")
|
|
1362
|
+
add(where, "range binding requires an integer target input");
|
|
1363
|
+
continue;
|
|
1364
|
+
}
|
|
1365
|
+
checkValue(value, declared.type, where, action.input, action);
|
|
812
1366
|
if (declared.type === "ref" && "field" in value) {
|
|
813
|
-
const supplied = field(value.field, action.input);
|
|
1367
|
+
const supplied = field(value.field, action.input, action);
|
|
814
1368
|
if (
|
|
815
1369
|
supplied?.type !== "ref" ||
|
|
1370
|
+
supplied.targetKind !== declared.targetKind ||
|
|
816
1371
|
!targetIds(supplied.target).every((id) =>
|
|
817
1372
|
targetIds(declared.target).includes(id),
|
|
818
1373
|
)
|
|
@@ -822,38 +1377,76 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
822
1377
|
}
|
|
823
1378
|
};
|
|
824
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
|
+
);
|
|
825
1393
|
const reference = expect(
|
|
826
1394
|
action.approval.target,
|
|
827
1395
|
"ref",
|
|
828
1396
|
where,
|
|
829
1397
|
action.input,
|
|
1398
|
+
action,
|
|
830
1399
|
);
|
|
831
1400
|
if (
|
|
832
1401
|
reference?.type === "ref" &&
|
|
1402
|
+
reference.targetKind === "instrument" &&
|
|
833
1403
|
targetIds(reference.target).some(
|
|
834
1404
|
(id) => !byId.get(id)?.actions[action.approval!.action],
|
|
835
1405
|
)
|
|
836
1406
|
)
|
|
837
1407
|
add(where, "approval target action does not exist");
|
|
838
|
-
if (reference?.type === "ref")
|
|
1408
|
+
if (reference?.type === "ref" && reference.targetKind === "instrument")
|
|
839
1409
|
for (const id of targetIds(reference.target))
|
|
840
1410
|
checkArguments(id, action.approval.action, action.approval.input);
|
|
841
|
-
expect(action.approval.expires, "date", where, action.input);
|
|
842
|
-
if (!document
|
|
843
|
-
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
|
+
);
|
|
844
1420
|
}
|
|
845
1421
|
const targets: { targets: string[]; count: number }[] = [];
|
|
846
1422
|
for (const call of action.invoke ?? []) {
|
|
1423
|
+
if (call.guard)
|
|
1424
|
+
checkRequirements([call.guard], where, action.input, action);
|
|
1425
|
+
const range = "instrument" in call ? call.range : undefined;
|
|
1426
|
+
if (range) {
|
|
1427
|
+
checkValue(range.count, "integer", where, action.input, action);
|
|
1428
|
+
if (["self", "input", "party", "subject"].includes(range.bind))
|
|
1429
|
+
add(where, "range binding cannot shadow a reserved scope");
|
|
1430
|
+
if (
|
|
1431
|
+
"literal" in range.count &&
|
|
1432
|
+
(typeof range.count.literal !== "number" ||
|
|
1433
|
+
range.count.literal < 1 ||
|
|
1434
|
+
range.count.literal > range.maximum)
|
|
1435
|
+
)
|
|
1436
|
+
add(where, "range count must be between one and its maximum");
|
|
1437
|
+
}
|
|
847
1438
|
if ("selection" in call)
|
|
848
1439
|
checkSelection(call.selection, where, action.input);
|
|
849
1440
|
const target =
|
|
850
|
-
"reference" in call
|
|
1441
|
+
"reference" in call
|
|
1442
|
+
? field(call.reference, action.input, action)
|
|
1443
|
+
: undefined;
|
|
851
1444
|
const selected =
|
|
852
1445
|
"instrument" in call
|
|
853
1446
|
? call.instrument
|
|
854
1447
|
: "selection" in call
|
|
855
1448
|
? call.selection.instrument
|
|
856
|
-
: target?.type === "ref"
|
|
1449
|
+
: target?.type === "ref" && target.targetKind === "instrument"
|
|
857
1450
|
? target.target
|
|
858
1451
|
: undefined;
|
|
859
1452
|
const ids = Array.isArray(selected)
|
|
@@ -863,18 +1456,46 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
863
1456
|
: [];
|
|
864
1457
|
if (!ids.length) add(where, "invocation needs a declared target");
|
|
865
1458
|
for (const targetId of ids) {
|
|
866
|
-
checkArguments(targetId, call.action, call.input);
|
|
1459
|
+
checkArguments(targetId, call.action, call.input, range?.bind);
|
|
867
1460
|
if (call.action === "create" && !("instrument" in call))
|
|
868
1461
|
add(
|
|
869
1462
|
where,
|
|
870
1463
|
"invocation references an existing instance and cannot create it again",
|
|
871
1464
|
);
|
|
872
|
-
|
|
1465
|
+
const targetInst = byId.get(targetId);
|
|
1466
|
+
if (!targetInst?.actions[call.action])
|
|
873
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
|
+
}
|
|
874
1494
|
}
|
|
875
1495
|
targets.push({
|
|
876
1496
|
targets: ids.map((id) => `${id}.${call.action}`),
|
|
877
|
-
count:
|
|
1497
|
+
count:
|
|
1498
|
+
"selection" in call ? call.selection.limit : (range?.maximum ?? 1),
|
|
878
1499
|
});
|
|
879
1500
|
}
|
|
880
1501
|
calls.set(`${inst.id}.${actionName}`, targets);
|
|
@@ -891,7 +1512,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
891
1512
|
const visit = (key: string): number => {
|
|
892
1513
|
if (active.has(key)) {
|
|
893
1514
|
add("$.instruments", `invocation cycle at ${key}`, "UDL2010");
|
|
894
|
-
return
|
|
1515
|
+
return MAX_ACTION_EXPANSION + 1;
|
|
895
1516
|
}
|
|
896
1517
|
const known = depths.get(key);
|
|
897
1518
|
if (known !== undefined) return known;
|
|
@@ -904,8 +1525,16 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
904
1525
|
);
|
|
905
1526
|
active.delete(key);
|
|
906
1527
|
depths.set(key, size);
|
|
907
|
-
|
|
908
|
-
|
|
1528
|
+
const [id, action] = key.split(".");
|
|
1529
|
+
const limit =
|
|
1530
|
+
document.instruments.find((item) => item.id === id)?.actions[action!]
|
|
1531
|
+
?.expansionLimit ?? MAX_ACTION_EXPANSION;
|
|
1532
|
+
if (size > limit)
|
|
1533
|
+
add(
|
|
1534
|
+
"$.instruments",
|
|
1535
|
+
`invocation ${key} exceeds ${limit} actions`,
|
|
1536
|
+
"UDL2010",
|
|
1537
|
+
);
|
|
909
1538
|
return size;
|
|
910
1539
|
};
|
|
911
1540
|
for (const key of calls.keys()) visit(key);
|