@hyperscale0/udl 3.2.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +11 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/evolution.d.ts.map +1 -1
- package/dist/evolution.js +5 -0
- package/dist/evolution.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/instrument-references.d.ts.map +1 -1
- package/dist/instrument-references.js +21 -1
- package/dist/instrument-references.js.map +1 -1
- package/dist/object-contract.d.ts +144 -0
- package/dist/object-contract.d.ts.map +1 -0
- package/dist/object-contract.js +279 -0
- package/dist/object-contract.js.map +1 -0
- package/dist/reporting.d.ts +0 -1
- package/dist/reporting.d.ts.map +1 -1
- package/dist/reporting.js +0 -1
- package/dist/reporting.js.map +1 -1
- package/dist/schema.d.ts +1815 -126
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +100 -33
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts +5 -2
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +403 -146
- package/dist/validation.js.map +1 -1
- package/docs/README.md +66 -17
- package/package.json +4 -4
- package/spec/README.md +66 -17
- package/spec/{darb.udl.json → cars.udl.json} +7 -7
- package/spec/udl.schema.json +2876 -6516
- package/src/diagnostics.ts +27 -1
- package/src/evolution.ts +8 -0
- package/src/index.ts +3 -0
- package/src/instrument-references.ts +25 -1
- package/src/object-contract.ts +464 -0
- package/src/reporting.ts +0 -1
- package/src/schema.ts +155 -33
- package/src/validation.ts +635 -174
package/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,59 @@ 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
|
+
|
|
15
65
|
export type UdlValidationResult =
|
|
16
66
|
| { ok: true; value: UdlDocument }
|
|
17
67
|
| { ok: false; issues: readonly UdlIssue[] };
|
|
@@ -58,7 +108,9 @@ const overlap = (a: string | string[], b: string | string[]) =>
|
|
|
58
108
|
targetIds(a).some((id) => targetIds(b).includes(id));
|
|
59
109
|
|
|
60
110
|
/** Union paths retain only fields with compatible types on every member. */
|
|
61
|
-
function commonField(
|
|
111
|
+
function commonField(
|
|
112
|
+
fields: (UdlField | UdlObjectField | undefined)[],
|
|
113
|
+
): UdlField | undefined {
|
|
62
114
|
const first = fields[0];
|
|
63
115
|
if (
|
|
64
116
|
!first ||
|
|
@@ -71,6 +123,16 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
|
|
|
71
123
|
)
|
|
72
124
|
return;
|
|
73
125
|
if (first.type === "ref") {
|
|
126
|
+
if (
|
|
127
|
+
fields.some(
|
|
128
|
+
(field) =>
|
|
129
|
+
field?.type !== "ref" ||
|
|
130
|
+
field.targetKind !== first.targetKind ||
|
|
131
|
+
JSON.stringify(field.targetFamily) !==
|
|
132
|
+
JSON.stringify(first.targetFamily),
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
return;
|
|
74
136
|
const targets = [
|
|
75
137
|
...new Set(
|
|
76
138
|
fields.flatMap((field) =>
|
|
@@ -78,7 +140,12 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
|
|
|
78
140
|
),
|
|
79
141
|
),
|
|
80
142
|
];
|
|
81
|
-
return {
|
|
143
|
+
return {
|
|
144
|
+
...first,
|
|
145
|
+
targetKind: first.targetKind,
|
|
146
|
+
target: targets.length === 1 ? targets[0]! : targets,
|
|
147
|
+
...(first.targetFamily ? { targetFamily: first.targetFamily } : {}),
|
|
148
|
+
} as UdlField;
|
|
82
149
|
}
|
|
83
150
|
if (
|
|
84
151
|
first.type === "account" &&
|
|
@@ -88,7 +155,6 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
|
|
|
88
155
|
field.book !== first.book ||
|
|
89
156
|
field.owner !== first.owner ||
|
|
90
157
|
field.key !== first.key ||
|
|
91
|
-
field.external !== first.external ||
|
|
92
158
|
field.contra !== first.contra,
|
|
93
159
|
)
|
|
94
160
|
)
|
|
@@ -109,11 +175,12 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
|
|
|
109
175
|
(field) =>
|
|
110
176
|
field?.type !== "list" ||
|
|
111
177
|
field.item !== first.item ||
|
|
178
|
+
field.targetKind !== first.targetKind ||
|
|
112
179
|
field.target !== first.target,
|
|
113
180
|
)
|
|
114
181
|
)
|
|
115
182
|
return;
|
|
116
|
-
return first;
|
|
183
|
+
return first as UdlField;
|
|
117
184
|
}
|
|
118
185
|
|
|
119
186
|
export function resolveField(
|
|
@@ -121,8 +188,71 @@ export function resolveField(
|
|
|
121
188
|
instrument: UdlInstrument,
|
|
122
189
|
path: string,
|
|
123
190
|
input: readonly UdlField[] = [],
|
|
191
|
+
action?: UdlAction | UdlActionSubject,
|
|
124
192
|
): UdlField | undefined {
|
|
125
|
-
return resolvePath(document, instrument, path, input, new Map());
|
|
193
|
+
return resolvePath(document, instrument, path, input, new Map(), action);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** A subject expression names a requirement; objectField names its stored metadata. */
|
|
197
|
+
export function resolveSubjectRequirement(
|
|
198
|
+
instrument: UdlInstrument,
|
|
199
|
+
name: string,
|
|
200
|
+
action?: UdlAction | UdlActionSubject,
|
|
201
|
+
): UdlSubjectRequirement | undefined {
|
|
202
|
+
const requirements = action
|
|
203
|
+
? "requirements" in action
|
|
204
|
+
? action.requirements
|
|
205
|
+
: action.subject?.requirements
|
|
206
|
+
: Object.values(instrument.actions).flatMap(
|
|
207
|
+
(item) => item.subject?.requirements ?? [],
|
|
208
|
+
);
|
|
209
|
+
return requirements?.find((requirement) => requirement.field.name === name);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function resolveReferencePath(
|
|
213
|
+
document: UdlDocument,
|
|
214
|
+
field: Extract<UdlField, { type: "ref" }>,
|
|
215
|
+
suffix: string,
|
|
216
|
+
cache: Map<string, UdlField | undefined>,
|
|
217
|
+
): UdlField | undefined {
|
|
218
|
+
return commonField(
|
|
219
|
+
targetIds(field.target).map((id) => {
|
|
220
|
+
const key = `${field.targetKind}:${id}:${suffix}`;
|
|
221
|
+
if (!cache.has(key)) {
|
|
222
|
+
const object =
|
|
223
|
+
field.targetKind === "object" &&
|
|
224
|
+
document.objects.find((item) => item.id === id);
|
|
225
|
+
const instrument =
|
|
226
|
+
field.targetKind === "instrument" &&
|
|
227
|
+
document.instruments.find((item) => item.id === id);
|
|
228
|
+
cache.set(
|
|
229
|
+
key,
|
|
230
|
+
object
|
|
231
|
+
? resolveObjectPath(document, object, suffix, cache)
|
|
232
|
+
: instrument
|
|
233
|
+
? resolvePath(document, instrument, `self.${suffix}`, [], cache)
|
|
234
|
+
: undefined,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return cache.get(key);
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function resolveObjectPath(
|
|
243
|
+
document: UdlDocument,
|
|
244
|
+
object: UdlObjectKind,
|
|
245
|
+
path: string,
|
|
246
|
+
cache: Map<string, UdlField | undefined>,
|
|
247
|
+
): UdlField | undefined {
|
|
248
|
+
const parts = path.split(".");
|
|
249
|
+
const key = parts.shift();
|
|
250
|
+
if (!key) return;
|
|
251
|
+
const field = object.fields.find((f) => f.name === key);
|
|
252
|
+
if (!field) return;
|
|
253
|
+
if (parts.length === 0) return field as UdlField;
|
|
254
|
+
if (field.type !== "ref") return;
|
|
255
|
+
return resolveReferencePath(document, field, parts.join("."), cache);
|
|
126
256
|
}
|
|
127
257
|
|
|
128
258
|
function resolvePath(
|
|
@@ -131,30 +261,73 @@ function resolvePath(
|
|
|
131
261
|
path: string,
|
|
132
262
|
input: readonly UdlField[],
|
|
133
263
|
cache: Map<string, UdlField | undefined>,
|
|
264
|
+
action?: UdlAction | UdlActionSubject,
|
|
134
265
|
): UdlField | undefined {
|
|
135
266
|
const parts = path.split(".");
|
|
136
267
|
const root = parts.shift();
|
|
137
268
|
const key = parts.shift();
|
|
138
269
|
if (!key)
|
|
139
270
|
return root === "self"
|
|
140
|
-
? {
|
|
271
|
+
? {
|
|
272
|
+
name: "id",
|
|
273
|
+
type: "ref",
|
|
274
|
+
targetKind: "instrument",
|
|
275
|
+
target: instrument.id,
|
|
276
|
+
}
|
|
141
277
|
: undefined;
|
|
142
278
|
if (root === "party") {
|
|
143
|
-
if (!document
|
|
279
|
+
if (!moneyParty(document, instrument, key)) return;
|
|
144
280
|
if (parts.length === 0)
|
|
145
281
|
return { name: key, type: "account", owner: key, book: "cash" };
|
|
146
282
|
if (parts.length === 1 && ["balance", "reserved"].includes(parts[0]!))
|
|
147
283
|
return { name: parts[0]!, type: "money" };
|
|
148
284
|
return;
|
|
149
285
|
}
|
|
286
|
+
if (root === "self" && key === "subject" && instrument.subject) {
|
|
287
|
+
if (parts.length === 0)
|
|
288
|
+
return {
|
|
289
|
+
name: "subject",
|
|
290
|
+
type: "ref",
|
|
291
|
+
targetKind: "object",
|
|
292
|
+
target: instrument.subject,
|
|
293
|
+
};
|
|
294
|
+
const target = document.objects.find((o) => o.id === instrument.subject);
|
|
295
|
+
if (!target) return;
|
|
296
|
+
const suffix = parts.join(".");
|
|
297
|
+
const cacheKey = `obj:${instrument.subject}:${suffix}`;
|
|
298
|
+
if (!cache.has(cacheKey))
|
|
299
|
+
cache.set(cacheKey, resolveObjectPath(document, target, suffix, cache));
|
|
300
|
+
return cache.get(cacheKey);
|
|
301
|
+
}
|
|
150
302
|
if (root === "self" && parts.length === 0) {
|
|
151
|
-
if (key === "id")
|
|
303
|
+
if (key === "id")
|
|
304
|
+
return {
|
|
305
|
+
name: key,
|
|
306
|
+
type: "ref",
|
|
307
|
+
targetKind: "instrument",
|
|
308
|
+
target: instrument.id,
|
|
309
|
+
};
|
|
152
310
|
if (key === "now" || key === "createdAt")
|
|
153
311
|
return { name: key, type: "date" };
|
|
154
312
|
if (key === "productRevision") return { name: key, type: "text" };
|
|
155
313
|
if (key === "status")
|
|
156
314
|
return { name: key, type: "enum", values: instrument.lifecycle.states };
|
|
157
315
|
}
|
|
316
|
+
if (root === "subject") {
|
|
317
|
+
const requirement = resolveSubjectRequirement(instrument, key, action);
|
|
318
|
+
if (!requirement || !instrument.subject) return;
|
|
319
|
+
const object = document.objects.find(
|
|
320
|
+
(item) => item.id === instrument.subject,
|
|
321
|
+
);
|
|
322
|
+
if (!object) return;
|
|
323
|
+
return resolveObjectPath(
|
|
324
|
+
document,
|
|
325
|
+
object,
|
|
326
|
+
[requirement.objectField ?? requirement.field.name, ...parts].join("."),
|
|
327
|
+
cache,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
158
331
|
const scope = instrument;
|
|
159
332
|
let field = (
|
|
160
333
|
root === "self" ? instrument.fields : root === "input" ? input : []
|
|
@@ -180,18 +353,11 @@ function resolvePath(
|
|
|
180
353
|
values: ["reserved", "posted", "settled", "reversed", "voided"],
|
|
181
354
|
};
|
|
182
355
|
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
|
-
}),
|
|
356
|
+
return resolveReferencePath(
|
|
357
|
+
document,
|
|
358
|
+
field,
|
|
359
|
+
parts.slice(index).join("."),
|
|
360
|
+
cache,
|
|
195
361
|
);
|
|
196
362
|
}
|
|
197
363
|
return field;
|
|
@@ -238,10 +404,237 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
238
404
|
seen.add(v);
|
|
239
405
|
}
|
|
240
406
|
};
|
|
407
|
+
if (document.objects.length === 0 && document.instruments.length === 0)
|
|
408
|
+
add("$", "document requires at least one object or instrument", "UDL2002");
|
|
409
|
+
|
|
410
|
+
duplicate(
|
|
411
|
+
document.objects.map((o) => o.id),
|
|
412
|
+
"$.objects",
|
|
413
|
+
);
|
|
241
414
|
duplicate(
|
|
242
415
|
document.instruments.map((i) => i.id),
|
|
243
416
|
"$.instruments",
|
|
244
417
|
);
|
|
418
|
+
|
|
419
|
+
for (const name of subjectPartyRoles)
|
|
420
|
+
if (document.parties[name])
|
|
421
|
+
add(
|
|
422
|
+
`$.parties.${name}`,
|
|
423
|
+
`${name} is a reserved subject role`,
|
|
424
|
+
"party_name_reserved",
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
const byObjectId = new Map(document.objects.map((o) => [o.id, o]));
|
|
428
|
+
const byId = new Map(document.instruments.map((i) => [i.id, i]));
|
|
429
|
+
|
|
430
|
+
const checkFields = (
|
|
431
|
+
fields: readonly (UdlField | UdlObjectField)[],
|
|
432
|
+
where: string,
|
|
433
|
+
isObject = false,
|
|
434
|
+
instrument?: UdlInstrument,
|
|
435
|
+
) => {
|
|
436
|
+
duplicate(
|
|
437
|
+
fields.map((f) => f.name),
|
|
438
|
+
where,
|
|
439
|
+
);
|
|
440
|
+
for (const f of fields) {
|
|
441
|
+
if (isObject) {
|
|
442
|
+
if (reservedObjectNamesSet.has(f.name))
|
|
443
|
+
add(where, `${f.name} is a reserved object name`);
|
|
444
|
+
} else {
|
|
445
|
+
if (f.type === "account" && f.contra && f.book !== "claim")
|
|
446
|
+
add(where, "contra accounts require the claim book");
|
|
447
|
+
if (
|
|
448
|
+
["id", "status", "createdAt", "now", "productRevision"].includes(
|
|
449
|
+
f.name,
|
|
450
|
+
)
|
|
451
|
+
)
|
|
452
|
+
add(where, `${f.name} is a sealed instance field`);
|
|
453
|
+
if (
|
|
454
|
+
f.type === "account" &&
|
|
455
|
+
f.owner !== "self" &&
|
|
456
|
+
!moneyParty(document, instrument, f.owner)
|
|
457
|
+
)
|
|
458
|
+
add(
|
|
459
|
+
where,
|
|
460
|
+
`${f.name} needs an eligible money party as owner`,
|
|
461
|
+
hasParty(document, instrument, f.owner)
|
|
462
|
+
? "party_kind_mismatch"
|
|
463
|
+
: "subject_party_unbound",
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
if (f.type === "text") {
|
|
467
|
+
if ((f.minLength ?? 1) > (f.maxLength ?? 2048))
|
|
468
|
+
add(where, "text minimum exceeds maximum");
|
|
469
|
+
if (f.pattern) {
|
|
470
|
+
try {
|
|
471
|
+
new RegExp(f.pattern);
|
|
472
|
+
} catch {
|
|
473
|
+
add(where, "invalid text character class");
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (f.type === "ref") {
|
|
478
|
+
duplicate(targetIds(f.target), where);
|
|
479
|
+
for (const id of targetIds(f.target)) {
|
|
480
|
+
const exists =
|
|
481
|
+
f.targetKind === "object" ? byObjectId.has(id) : byId.has(id);
|
|
482
|
+
if (!exists) add(where, `unknown reference target ${id}`, "UDL5001");
|
|
483
|
+
}
|
|
484
|
+
if (f.targetFamily) {
|
|
485
|
+
if (f.targetKind !== "instrument") {
|
|
486
|
+
add(
|
|
487
|
+
where,
|
|
488
|
+
"targetFamily requires an instrument reference target",
|
|
489
|
+
"UDL5001",
|
|
490
|
+
);
|
|
491
|
+
} else {
|
|
492
|
+
for (const id of targetIds(f.target)) {
|
|
493
|
+
const targetInst = byId.get(id);
|
|
494
|
+
if (
|
|
495
|
+
targetInst &&
|
|
496
|
+
(!targetInst.family ||
|
|
497
|
+
targetInst.family.module !== f.targetFamily.module ||
|
|
498
|
+
targetInst.family.exportPath !== f.targetFamily.exportPath ||
|
|
499
|
+
targetInst.family.revision !== f.targetFamily.revision)
|
|
500
|
+
) {
|
|
501
|
+
add(
|
|
502
|
+
where,
|
|
503
|
+
`targetFamily does not match target instrument ${id} family`,
|
|
504
|
+
"UDL5001",
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (f.type === "enum") {
|
|
512
|
+
duplicate(f.values, where);
|
|
513
|
+
if (f.value !== undefined && !f.values.includes(f.value))
|
|
514
|
+
add(where, `${f.name} constant is outside its enum`);
|
|
515
|
+
}
|
|
516
|
+
if (f.type === "list") {
|
|
517
|
+
const validRefList =
|
|
518
|
+
f.item === "ref" &&
|
|
519
|
+
f.targetKind &&
|
|
520
|
+
(f.targetKind === "object"
|
|
521
|
+
? byObjectId.has(f.target ?? "")
|
|
522
|
+
: byId.has(f.target ?? ""));
|
|
523
|
+
const nonRefTarget =
|
|
524
|
+
f.item !== "ref" &&
|
|
525
|
+
(f.targetKind !== undefined || f.target !== undefined);
|
|
526
|
+
if (!validRefList && (f.item === "ref" || nonRefTarget))
|
|
527
|
+
add(
|
|
528
|
+
where,
|
|
529
|
+
`${f.name} needs a target exactly when its items are references`,
|
|
530
|
+
);
|
|
531
|
+
if (f.targetFamily) {
|
|
532
|
+
if (f.item !== "ref" || f.targetKind !== "instrument") {
|
|
533
|
+
add(
|
|
534
|
+
where,
|
|
535
|
+
"targetFamily requires an instrument reference target",
|
|
536
|
+
"UDL5001",
|
|
537
|
+
);
|
|
538
|
+
} else if (f.target) {
|
|
539
|
+
const targetInst = byId.get(f.target);
|
|
540
|
+
if (
|
|
541
|
+
targetInst &&
|
|
542
|
+
(!targetInst.family ||
|
|
543
|
+
targetInst.family.module !== f.targetFamily.module ||
|
|
544
|
+
targetInst.family.exportPath !== f.targetFamily.exportPath ||
|
|
545
|
+
targetInst.family.revision !== f.targetFamily.revision)
|
|
546
|
+
) {
|
|
547
|
+
add(
|
|
548
|
+
where,
|
|
549
|
+
`targetFamily does not match target instrument ${f.target} family`,
|
|
550
|
+
"UDL5001",
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (
|
|
557
|
+
(f.type === "integer" || f.type === "money") &&
|
|
558
|
+
f.minimum !== undefined &&
|
|
559
|
+
f.maximum !== undefined &&
|
|
560
|
+
BigInt(f.minimum) > BigInt(f.maximum)
|
|
561
|
+
)
|
|
562
|
+
add(where, `${f.name} minimum exceeds maximum`);
|
|
563
|
+
if (
|
|
564
|
+
(f.type === "integer" || f.type === "money") &&
|
|
565
|
+
f.value !== undefined &&
|
|
566
|
+
((f.minimum !== undefined && BigInt(f.value) < BigInt(f.minimum)) ||
|
|
567
|
+
(f.maximum !== undefined && BigInt(f.value) > BigInt(f.maximum)))
|
|
568
|
+
)
|
|
569
|
+
add(where, `${f.name} constant violates its bounds`);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
for (const [index, obj] of document.objects.entries()) {
|
|
574
|
+
const base = `$.objects[${index}]`;
|
|
575
|
+
if (reservedObjectNamesSet.has(obj.id))
|
|
576
|
+
add(base, `${obj.id} is a reserved object name`);
|
|
577
|
+
duplicate(
|
|
578
|
+
obj.attachments.map((attachment) => attachment.name),
|
|
579
|
+
`${base}.attachments`,
|
|
580
|
+
);
|
|
581
|
+
duplicate(
|
|
582
|
+
obj.attachments.map((attachment) => attachment.instrument),
|
|
583
|
+
`${base}.attachments`,
|
|
584
|
+
);
|
|
585
|
+
for (const [attachmentIndex, attachment] of obj.attachments.entries()) {
|
|
586
|
+
for (const [parameter, binding] of Object.entries(attachment.parties))
|
|
587
|
+
if ("party" in binding && !document.parties[binding.party])
|
|
588
|
+
add(
|
|
589
|
+
`${base}.attachments[${attachmentIndex}].parties.${parameter}`,
|
|
590
|
+
`unknown declared party ${binding.party}`,
|
|
591
|
+
"subject_party_unbound",
|
|
592
|
+
);
|
|
593
|
+
if (byId.get(attachment.instrument)?.subject !== obj.id)
|
|
594
|
+
add(
|
|
595
|
+
`${base}.attachments`,
|
|
596
|
+
`${attachment.instrument} must attach to ${obj.id}`,
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
duplicate(obj.authoredFields, `${base}.authoredFields`);
|
|
600
|
+
duplicate(obj.columns, `${base}.columns`);
|
|
601
|
+
checkFields(obj.fields, `${base}.fields`, true);
|
|
602
|
+
for (let i = 0; i < obj.authoredFields.length; i++) {
|
|
603
|
+
const af = obj.authoredFields[i]!;
|
|
604
|
+
if (reservedObjectNamesSet.has(af))
|
|
605
|
+
add(`${base}.authoredFields`, `${af} is a reserved object name`);
|
|
606
|
+
if (!obj.fields.some((f) => f.name === af))
|
|
607
|
+
add(
|
|
608
|
+
`${base}.authoredFields`,
|
|
609
|
+
`authored field ${af} is not in declared fields`,
|
|
610
|
+
"subject_field_unknown",
|
|
611
|
+
);
|
|
612
|
+
if (obj.fields[i]?.name !== af)
|
|
613
|
+
add(
|
|
614
|
+
base,
|
|
615
|
+
`authored field ${af} must precede attached requirements in fields`,
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
for (const col of obj.columns) {
|
|
619
|
+
if (reservedObjectNamesSet.has(col))
|
|
620
|
+
add(`${base}.columns`, `${col} is a reserved object name`);
|
|
621
|
+
if (!obj.fields.some((f) => f.name === col))
|
|
622
|
+
add(
|
|
623
|
+
`${base}.columns`,
|
|
624
|
+
`column ${col} is not a declared field`,
|
|
625
|
+
"subject_field_unknown",
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
const pubActions = document.instruments
|
|
629
|
+
.filter((i) => i.subject === obj.id)
|
|
630
|
+
.flatMap((i) =>
|
|
631
|
+
Object.values(i.actions).flatMap((a) =>
|
|
632
|
+
a.publicAction ? [a.publicAction] : [],
|
|
633
|
+
),
|
|
634
|
+
);
|
|
635
|
+
duplicate(pubActions, `$.objects.${obj.id}.publicActions`);
|
|
636
|
+
}
|
|
637
|
+
|
|
245
638
|
const reportIds = new Set<string>();
|
|
246
639
|
for (const [index, instrument] of document.instruments.entries()) {
|
|
247
640
|
for (const report of instrument.reports ?? []) {
|
|
@@ -259,19 +652,34 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
259
652
|
}
|
|
260
653
|
}
|
|
261
654
|
}
|
|
262
|
-
|
|
655
|
+
|
|
263
656
|
const calls = new Map<string, { targets: string[]; count: number }[]>();
|
|
264
657
|
for (const [index, inst] of document.instruments.entries()) {
|
|
265
658
|
const base = `$.instruments[${index}]`;
|
|
266
|
-
|
|
267
|
-
|
|
659
|
+
if (inst.subject && !byObjectId.has(inst.subject))
|
|
660
|
+
add(base, `unknown subject object ${inst.subject}`);
|
|
661
|
+
const field = (
|
|
662
|
+
p: string,
|
|
663
|
+
input: readonly UdlField[] = [],
|
|
664
|
+
action?: UdlAction | UdlActionSubject,
|
|
665
|
+
) => resolveField(document, inst, p, input, action);
|
|
268
666
|
const expect = (
|
|
269
667
|
p: string,
|
|
270
668
|
type: UdlField["type"],
|
|
271
669
|
where: string,
|
|
272
670
|
input: readonly UdlField[] = [],
|
|
671
|
+
action?: UdlAction | UdlActionSubject,
|
|
273
672
|
) => {
|
|
274
|
-
const
|
|
673
|
+
const party = p.startsWith("party.") ? p.split(".")[1]! : undefined;
|
|
674
|
+
if (party && !moneyParty(document, inst, party))
|
|
675
|
+
add(
|
|
676
|
+
where,
|
|
677
|
+
`${party} cannot supply a money account`,
|
|
678
|
+
hasParty(document, inst, party)
|
|
679
|
+
? "party_kind_mismatch"
|
|
680
|
+
: "subject_party_unbound",
|
|
681
|
+
);
|
|
682
|
+
const found = field(p, input, action);
|
|
275
683
|
if (found?.type !== type)
|
|
276
684
|
add(where, `${p} must name a ${type} field`, "UDL5001");
|
|
277
685
|
return found;
|
|
@@ -281,9 +689,10 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
281
689
|
type: UdlField["type"],
|
|
282
690
|
where: string,
|
|
283
691
|
input: readonly UdlField[] = [],
|
|
692
|
+
action?: UdlAction | UdlActionSubject,
|
|
284
693
|
) => {
|
|
285
694
|
if ("field" in v) {
|
|
286
|
-
expect(v.field, type, where, input);
|
|
695
|
+
expect(v.field, type, where, input, action);
|
|
287
696
|
return;
|
|
288
697
|
}
|
|
289
698
|
const valid =
|
|
@@ -299,82 +708,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
299
708
|
: typeof v.literal === "string";
|
|
300
709
|
if (!valid) add(where, `literal must have type ${type}`);
|
|
301
710
|
};
|
|
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`);
|
|
711
|
+
checkFields(inst.fields, `${base}.fields`, false, inst);
|
|
378
712
|
const captures = new Set(
|
|
379
713
|
Object.values(inst.actions).flatMap((a) => [
|
|
380
714
|
...a.moves.flatMap((m) =>
|
|
@@ -409,9 +743,24 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
409
743
|
a &&
|
|
410
744
|
b &&
|
|
411
745
|
a.type === b.type &&
|
|
412
|
-
(a.type !== "ref" ||
|
|
746
|
+
(a.type !== "ref" ||
|
|
747
|
+
(b.type === "ref" &&
|
|
748
|
+
a.targetKind === b.targetKind &&
|
|
749
|
+
overlap(a.target, b.target)));
|
|
413
750
|
const anchor = field(selection.anchor, input);
|
|
414
751
|
for (const target of targets) {
|
|
752
|
+
if (
|
|
753
|
+
selection.family &&
|
|
754
|
+
(!target.family ||
|
|
755
|
+
target.family.module !== selection.family.module ||
|
|
756
|
+
target.family.exportPath !== selection.family.exportPath ||
|
|
757
|
+
target.family.revision !== selection.family.revision)
|
|
758
|
+
)
|
|
759
|
+
add(
|
|
760
|
+
where,
|
|
761
|
+
`selection family does not match target instrument ${target.id} family`,
|
|
762
|
+
"UDL5001",
|
|
763
|
+
);
|
|
415
764
|
if (
|
|
416
765
|
selection.window &&
|
|
417
766
|
target.fields.find((f) => f.name === selection.window!.field)
|
|
@@ -471,6 +820,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
471
820
|
calculations: readonly UdlCalculation[],
|
|
472
821
|
where: string,
|
|
473
822
|
input: readonly UdlField[] = [],
|
|
823
|
+
act?: UdlAction,
|
|
474
824
|
) => {
|
|
475
825
|
duplicate(
|
|
476
826
|
calculations.map((c) => c.target),
|
|
@@ -484,7 +834,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
484
834
|
const resultType =
|
|
485
835
|
c.op === "at"
|
|
486
836
|
? (() => {
|
|
487
|
-
const list = field(c.list, input);
|
|
837
|
+
const list = field(c.list, input, act);
|
|
488
838
|
return list?.type === "list" ? list.item : "text";
|
|
489
839
|
})()
|
|
490
840
|
: (c.op === "aggregate" && c.measure === "count") ||
|
|
@@ -499,11 +849,11 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
499
849
|
const operands: UdlValue[] = [];
|
|
500
850
|
const money = (v: UdlValue) => {
|
|
501
851
|
operands.push(v);
|
|
502
|
-
checkValue(v, arithmetic ? resultType : "money", where, input);
|
|
852
|
+
checkValue(v, arithmetic ? resultType : "money", where, input, act);
|
|
503
853
|
};
|
|
504
854
|
if (c.op === "at") {
|
|
505
|
-
expect(c.list, "list", where, input);
|
|
506
|
-
checkValue(c.position, "integer", where, input);
|
|
855
|
+
expect(c.list, "list", where, input, act);
|
|
856
|
+
checkValue(c.position, "integer", where, input, act);
|
|
507
857
|
if ("literal" in c.position && Number(c.position.literal) < 1)
|
|
508
858
|
add(where, "list positions start at one");
|
|
509
859
|
operands.push({ field: c.list }, c.position);
|
|
@@ -522,15 +872,15 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
522
872
|
money(c.amount);
|
|
523
873
|
const type =
|
|
524
874
|
"field" in c.numerator
|
|
525
|
-
? field(c.numerator.field, input)?.type
|
|
875
|
+
? field(c.numerator.field, input, act)?.type
|
|
526
876
|
: typeof c.numerator.literal === "number"
|
|
527
877
|
? "integer"
|
|
528
878
|
: "money";
|
|
529
879
|
if (type !== "integer" && type !== "money" && type !== "percent")
|
|
530
880
|
add(where, "ratio weights must share a numeric type");
|
|
531
881
|
else {
|
|
532
|
-
checkValue(c.numerator, type, where, input);
|
|
533
|
-
checkValue(c.denominator, type, where, input);
|
|
882
|
+
checkValue(c.numerator, type, where, input, act);
|
|
883
|
+
checkValue(c.denominator, type, where, input, act);
|
|
534
884
|
}
|
|
535
885
|
if (
|
|
536
886
|
"literal" in c.denominator &&
|
|
@@ -546,13 +896,13 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
546
896
|
}
|
|
547
897
|
if (c.op === "rate") {
|
|
548
898
|
money(c.base);
|
|
549
|
-
checkValue(c.bps, "percent", where, input);
|
|
899
|
+
checkValue(c.bps, "percent", where, input, act);
|
|
550
900
|
operands.push(c.bps);
|
|
551
901
|
}
|
|
552
902
|
if (c.op === "multiply" || c.op === "divide") {
|
|
553
903
|
money(c.amount);
|
|
554
904
|
const count = c.op === "multiply" ? c.units : c.divisor;
|
|
555
|
-
checkValue(count, "integer", where, input);
|
|
905
|
+
checkValue(count, "integer", where, input, act);
|
|
556
906
|
operands.push(count);
|
|
557
907
|
if (
|
|
558
908
|
"literal" in count &&
|
|
@@ -566,8 +916,8 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
566
916
|
);
|
|
567
917
|
}
|
|
568
918
|
if (c.op === "shift") {
|
|
569
|
-
checkValue(c.date, "date", where, input);
|
|
570
|
-
checkValue(c.milliseconds, "duration", where, input);
|
|
919
|
+
checkValue(c.date, "date", where, input, act);
|
|
920
|
+
checkValue(c.milliseconds, "duration", where, input, act);
|
|
571
921
|
operands.push(c.date, c.milliseconds);
|
|
572
922
|
}
|
|
573
923
|
dependencies.set(
|
|
@@ -628,13 +978,16 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
628
978
|
requirements: typeof inst.invariants,
|
|
629
979
|
where: string,
|
|
630
980
|
input: readonly UdlField[] = [],
|
|
981
|
+
act?: UdlAction,
|
|
631
982
|
) => {
|
|
632
983
|
for (const req of requirements ?? []) {
|
|
633
984
|
if (req.kind === "compare") {
|
|
634
985
|
const left =
|
|
635
|
-
"field" in req.left ? field(req.left.field, input) : undefined;
|
|
986
|
+
"field" in req.left ? field(req.left.field, input, act) : undefined;
|
|
636
987
|
const right =
|
|
637
|
-
"field" in req.right
|
|
988
|
+
"field" in req.right
|
|
989
|
+
? field(req.right.field, input, act)
|
|
990
|
+
: undefined;
|
|
638
991
|
if (
|
|
639
992
|
("field" in req.left && !left) ||
|
|
640
993
|
("field" in req.right && !right)
|
|
@@ -646,11 +999,12 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
646
999
|
(left.type !== right.type ||
|
|
647
1000
|
(left.type === "ref" &&
|
|
648
1001
|
right.type === "ref" &&
|
|
649
|
-
|
|
1002
|
+
(left.targetKind !== right.targetKind ||
|
|
1003
|
+
!overlap(left.target, right.target))))
|
|
650
1004
|
)
|
|
651
1005
|
add(where, "comparison operands have different types");
|
|
652
1006
|
if (left && "literal" in req.right) {
|
|
653
|
-
checkValue(req.right, left.type, where, input);
|
|
1007
|
+
checkValue(req.right, left.type, where, input, act);
|
|
654
1008
|
if (
|
|
655
1009
|
left.type === "enum" &&
|
|
656
1010
|
!left.values.includes(String(req.right.literal))
|
|
@@ -658,7 +1012,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
658
1012
|
add(where, "comparison names an undeclared enum value");
|
|
659
1013
|
}
|
|
660
1014
|
if (right && "literal" in req.left) {
|
|
661
|
-
checkValue(req.left, right.type, where, input);
|
|
1015
|
+
checkValue(req.left, right.type, where, input, act);
|
|
662
1016
|
if (
|
|
663
1017
|
right.type === "enum" &&
|
|
664
1018
|
!right.values.includes(String(req.left.literal))
|
|
@@ -666,9 +1020,10 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
666
1020
|
add(where, "comparison names an undeclared enum value");
|
|
667
1021
|
}
|
|
668
1022
|
} else if (req.kind === "state") {
|
|
669
|
-
const target = expect(req.reference, "ref", where, input);
|
|
1023
|
+
const target = expect(req.reference, "ref", where, input, act);
|
|
670
1024
|
if (
|
|
671
1025
|
target?.type === "ref" &&
|
|
1026
|
+
target.targetKind === "instrument" &&
|
|
672
1027
|
req.states.some((s) =>
|
|
673
1028
|
targetIds(target.target).some(
|
|
674
1029
|
(id) => !byId.get(id)?.lifecycle.states.includes(s),
|
|
@@ -678,28 +1033,27 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
678
1033
|
add(where, "reference gate names an undeclared state");
|
|
679
1034
|
} else if (req.kind === "unique") {
|
|
680
1035
|
for (const p of req.fields)
|
|
681
|
-
if (!field(p, input
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
if (
|
|
685
|
-
|
|
1036
|
+
if (!field(p, input, act))
|
|
1037
|
+
add(where, `unknown identity field ${p}`);
|
|
1038
|
+
} else if (req.kind === "evidence") {
|
|
1039
|
+
if (req.instruction) {
|
|
1040
|
+
expect(req.instruction, "text", where, input, act);
|
|
1041
|
+
if (!["confirmed", "rejected"].includes(req.result))
|
|
1042
|
+
add(where, "instruction evidence requires a terminal outcome");
|
|
1043
|
+
}
|
|
1044
|
+
const subject = field(req.subject, input, act);
|
|
686
1045
|
if (
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
(id) => !byId.get(id)?.actions[req.action!],
|
|
1046
|
+
!subject ||
|
|
1047
|
+
!["account", "text", ...(req.instruction ? ["ref"] : [])].includes(
|
|
1048
|
+
subject.type,
|
|
691
1049
|
)
|
|
692
1050
|
)
|
|
693
|
-
add(where, `unknown approved action ${req.action}`);
|
|
694
|
-
} else if (req.kind === "evidence") {
|
|
695
|
-
const subject = field(req.subject, input);
|
|
696
|
-
if (!subject || !["account", "text"].includes(subject.type))
|
|
697
1051
|
add(
|
|
698
1052
|
where,
|
|
699
1053
|
"evidence subject must be an account or text subject id",
|
|
700
1054
|
);
|
|
701
1055
|
} else if (req.kind === "hours") {
|
|
702
|
-
expect(req.at, "date", where, input);
|
|
1056
|
+
expect(req.at, "date", where, input, act);
|
|
703
1057
|
try {
|
|
704
1058
|
new Intl.DateTimeFormat("en", { timeZone: req.timezone });
|
|
705
1059
|
} catch {
|
|
@@ -723,6 +1077,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
723
1077
|
req.measure === "count" ? "integer" : "money",
|
|
724
1078
|
where,
|
|
725
1079
|
input,
|
|
1080
|
+
act,
|
|
726
1081
|
);
|
|
727
1082
|
}
|
|
728
1083
|
}
|
|
@@ -733,11 +1088,83 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
733
1088
|
const where = `${base}.actions.${actionName}`;
|
|
734
1089
|
if (actionName !== "create" && !inst.lifecycle.transitions[actionName])
|
|
735
1090
|
add(where, "action needs a lifecycle transition", "UDL3001");
|
|
736
|
-
|
|
1091
|
+
if (action.subject) {
|
|
1092
|
+
const subBase = `${where}.subject`;
|
|
1093
|
+
checkFields(
|
|
1094
|
+
action.subject.requirements.map((r) => ({
|
|
1095
|
+
...r.field,
|
|
1096
|
+
name: r.objectField ?? r.field.name,
|
|
1097
|
+
})),
|
|
1098
|
+
`${subBase}.requirements`,
|
|
1099
|
+
true,
|
|
1100
|
+
);
|
|
1101
|
+
for (const req of action.subject.requirements) {
|
|
1102
|
+
if (req.field.optional)
|
|
1103
|
+
add(
|
|
1104
|
+
`${subBase}.requirements`,
|
|
1105
|
+
`subject requirement ${req.field.name} cannot be optional`,
|
|
1106
|
+
"UDL2002",
|
|
1107
|
+
);
|
|
1108
|
+
const name = req.objectField ?? req.field.name;
|
|
1109
|
+
if (reservedObjectNamesSet.has(name))
|
|
1110
|
+
add(subBase, `${name} is a reserved object name`);
|
|
1111
|
+
if (inst.subject) {
|
|
1112
|
+
const field = byObjectId
|
|
1113
|
+
.get(inst.subject)
|
|
1114
|
+
?.fields.find((field) => field.name === name);
|
|
1115
|
+
if (!field || !sameObjectField(field, req.field))
|
|
1116
|
+
add(
|
|
1117
|
+
subBase,
|
|
1118
|
+
`object field ${name} must match requirement ${req.field.name}`,
|
|
1119
|
+
field ? "subject_field_conflict" : "subject_field_unknown",
|
|
1120
|
+
);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
duplicate(
|
|
1124
|
+
action.subject.adapters.map((a) => a.binding),
|
|
1125
|
+
`${subBase}.adapters`,
|
|
1126
|
+
);
|
|
1127
|
+
for (const adapter of action.subject.adapters) {
|
|
1128
|
+
if (adapter.snapshot) {
|
|
1129
|
+
checkFields(
|
|
1130
|
+
adapter.snapshot.requirements,
|
|
1131
|
+
`${subBase}.adapters.${adapter.binding}.requirements`,
|
|
1132
|
+
true,
|
|
1133
|
+
);
|
|
1134
|
+
for (const reqField of adapter.snapshot.requirements) {
|
|
1135
|
+
const objectName =
|
|
1136
|
+
adapter.renames?.[reqField.name] ?? reqField.name;
|
|
1137
|
+
const requirement = action.subject.requirements.find(
|
|
1138
|
+
(item) =>
|
|
1139
|
+
(item.objectField ?? item.field.name) === objectName &&
|
|
1140
|
+
sameObjectField(item.field, reqField),
|
|
1141
|
+
);
|
|
1142
|
+
if (!requirement)
|
|
1143
|
+
add(
|
|
1144
|
+
subBase,
|
|
1145
|
+
`adapter ${adapter.binding} requirement ${reqField.name} is absent from the action`,
|
|
1146
|
+
);
|
|
1147
|
+
if (reqField.optional)
|
|
1148
|
+
add(
|
|
1149
|
+
`${subBase}.adapters.${adapter.binding}`,
|
|
1150
|
+
`adapter requirement ${reqField.name} cannot be optional`,
|
|
1151
|
+
"UDL2002",
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
checkFields(action.input, `${where}.input`, false, inst);
|
|
737
1158
|
if (action.reminder) {
|
|
738
|
-
expect(action.reminder.installment, "ref", where, action.input);
|
|
739
|
-
expect(
|
|
740
|
-
|
|
1159
|
+
expect(action.reminder.installment, "ref", where, action.input, action);
|
|
1160
|
+
expect(
|
|
1161
|
+
action.reminder.recipient,
|
|
1162
|
+
"account",
|
|
1163
|
+
where,
|
|
1164
|
+
action.input,
|
|
1165
|
+
action,
|
|
1166
|
+
);
|
|
1167
|
+
expect(action.reminder.dueAt, "date", where, action.input, action);
|
|
741
1168
|
if (action.reminder.startHour >= action.reminder.endHour)
|
|
742
1169
|
add(where, "reminder sending window is empty");
|
|
743
1170
|
try {
|
|
@@ -773,12 +1200,18 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
773
1200
|
action.calculate ?? [],
|
|
774
1201
|
`${where}.calculate`,
|
|
775
1202
|
action.input,
|
|
1203
|
+
action,
|
|
1204
|
+
);
|
|
1205
|
+
checkRequirements(
|
|
1206
|
+
action.requires,
|
|
1207
|
+
`${where}.requires`,
|
|
1208
|
+
action.input,
|
|
1209
|
+
action,
|
|
776
1210
|
);
|
|
777
|
-
checkRequirements(action.requires, `${where}.requires`, action.input);
|
|
778
1211
|
if (
|
|
779
1212
|
typeof action.actor === "object" &&
|
|
780
1213
|
"party" in action.actor &&
|
|
781
|
-
!document
|
|
1214
|
+
!hasParty(document, inst, action.actor.party)
|
|
782
1215
|
)
|
|
783
1216
|
add(where, "actor names an undeclared party");
|
|
784
1217
|
if (
|
|
@@ -791,7 +1224,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
791
1224
|
add(where, "clock action needs a due instant");
|
|
792
1225
|
for (const clock of [action.due, action.deadline])
|
|
793
1226
|
if (clock) {
|
|
794
|
-
expect(clock.at, "date", where, action.input);
|
|
1227
|
+
expect(clock.at, "date", where, action.input, action);
|
|
795
1228
|
if (clock.localDay) {
|
|
796
1229
|
try {
|
|
797
1230
|
new Intl.DateTimeFormat("en", {
|
|
@@ -803,10 +1236,28 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
803
1236
|
}
|
|
804
1237
|
}
|
|
805
1238
|
for (const move of action.moves) {
|
|
1239
|
+
if (
|
|
1240
|
+
move.operation === "internal_transfer.reserve" &&
|
|
1241
|
+
move.boundary &&
|
|
1242
|
+
!action.subject?.adapters.some(
|
|
1243
|
+
(binding) =>
|
|
1244
|
+
binding.binding === move.boundary!.adapter && binding.snapshot,
|
|
1245
|
+
)
|
|
1246
|
+
)
|
|
1247
|
+
add(
|
|
1248
|
+
where,
|
|
1249
|
+
"boundary reservation requires a retained adapter binding",
|
|
1250
|
+
);
|
|
806
1251
|
if ("amount" in move) {
|
|
807
|
-
checkValue(move.amount, "money", where, action.input);
|
|
808
|
-
const from = expect(
|
|
809
|
-
|
|
1252
|
+
checkValue(move.amount, "money", where, action.input, action);
|
|
1253
|
+
const from = expect(
|
|
1254
|
+
move.from,
|
|
1255
|
+
"account",
|
|
1256
|
+
where,
|
|
1257
|
+
action.input,
|
|
1258
|
+
action,
|
|
1259
|
+
);
|
|
1260
|
+
const to = expect(move.to, "account", where, action.input, action);
|
|
810
1261
|
if (
|
|
811
1262
|
from?.type === "account" &&
|
|
812
1263
|
to?.type === "account" &&
|
|
@@ -822,7 +1273,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
822
1273
|
(from.key ?? "balance") === (to.key ?? "balance");
|
|
823
1274
|
if (move.from === move.to || samePartyAccount)
|
|
824
1275
|
add(where, "a transfer needs distinct accounts", "UDL4001");
|
|
825
|
-
} else expect(move.transfer, "text", where, action.input);
|
|
1276
|
+
} else expect(move.transfer, "text", where, action.input, action);
|
|
826
1277
|
}
|
|
827
1278
|
duplicate(
|
|
828
1279
|
action.moves.flatMap((m) =>
|
|
@@ -845,7 +1296,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
845
1296
|
inst.calculate.some((c) => c.target === key)
|
|
846
1297
|
)
|
|
847
1298
|
add(where, `cannot write immutable field ${key}`);
|
|
848
|
-
else checkValue(v, target.type, where, action.input);
|
|
1299
|
+
else checkValue(v, target.type, where, action.input, action);
|
|
849
1300
|
}
|
|
850
1301
|
const checkArguments = (
|
|
851
1302
|
targetId: string | undefined,
|
|
@@ -875,11 +1326,12 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
875
1326
|
add(where, "range binding requires an integer target input");
|
|
876
1327
|
continue;
|
|
877
1328
|
}
|
|
878
|
-
checkValue(value, declared.type, where, action.input);
|
|
1329
|
+
checkValue(value, declared.type, where, action.input, action);
|
|
879
1330
|
if (declared.type === "ref" && "field" in value) {
|
|
880
|
-
const supplied = field(value.field, action.input);
|
|
1331
|
+
const supplied = field(value.field, action.input, action);
|
|
881
1332
|
if (
|
|
882
1333
|
supplied?.type !== "ref" ||
|
|
1334
|
+
supplied.targetKind !== declared.targetKind ||
|
|
883
1335
|
!targetIds(supplied.target).every((id) =>
|
|
884
1336
|
targetIds(declared.target).includes(id),
|
|
885
1337
|
)
|
|
@@ -888,34 +1340,14 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
888
1340
|
}
|
|
889
1341
|
}
|
|
890
1342
|
};
|
|
891
|
-
if (action.approval) {
|
|
892
|
-
const reference = expect(
|
|
893
|
-
action.approval.target,
|
|
894
|
-
"ref",
|
|
895
|
-
where,
|
|
896
|
-
action.input,
|
|
897
|
-
);
|
|
898
|
-
if (
|
|
899
|
-
reference?.type === "ref" &&
|
|
900
|
-
targetIds(reference.target).some(
|
|
901
|
-
(id) => !byId.get(id)?.actions[action.approval!.action],
|
|
902
|
-
)
|
|
903
|
-
)
|
|
904
|
-
add(where, "approval target action does not exist");
|
|
905
|
-
if (reference?.type === "ref")
|
|
906
|
-
for (const id of targetIds(reference.target))
|
|
907
|
-
checkArguments(id, action.approval.action, action.approval.input);
|
|
908
|
-
expect(action.approval.expires, "date", where, action.input);
|
|
909
|
-
if (!document.parties[action.approval.party])
|
|
910
|
-
add(where, "approval needs a declared party");
|
|
911
|
-
}
|
|
912
1343
|
const targets: { targets: string[]; count: number }[] = [];
|
|
913
1344
|
for (const call of action.invoke ?? []) {
|
|
914
|
-
if (call.guard)
|
|
1345
|
+
if (call.guard)
|
|
1346
|
+
checkRequirements([call.guard], where, action.input, action);
|
|
915
1347
|
const range = "instrument" in call ? call.range : undefined;
|
|
916
1348
|
if (range) {
|
|
917
|
-
checkValue(range.count, "integer", where, action.input);
|
|
918
|
-
if (["self", "input", "party"].includes(range.bind))
|
|
1349
|
+
checkValue(range.count, "integer", where, action.input, action);
|
|
1350
|
+
if (["self", "input", "party", "subject"].includes(range.bind))
|
|
919
1351
|
add(where, "range binding cannot shadow a reserved scope");
|
|
920
1352
|
if (
|
|
921
1353
|
"literal" in range.count &&
|
|
@@ -928,13 +1360,15 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
928
1360
|
if ("selection" in call)
|
|
929
1361
|
checkSelection(call.selection, where, action.input);
|
|
930
1362
|
const target =
|
|
931
|
-
"reference" in call
|
|
1363
|
+
"reference" in call
|
|
1364
|
+
? field(call.reference, action.input, action)
|
|
1365
|
+
: undefined;
|
|
932
1366
|
const selected =
|
|
933
1367
|
"instrument" in call
|
|
934
1368
|
? call.instrument
|
|
935
1369
|
: "selection" in call
|
|
936
1370
|
? call.selection.instrument
|
|
937
|
-
: target?.type === "ref"
|
|
1371
|
+
: target?.type === "ref" && target.targetKind === "instrument"
|
|
938
1372
|
? target.target
|
|
939
1373
|
: undefined;
|
|
940
1374
|
const ids = Array.isArray(selected)
|
|
@@ -950,8 +1384,35 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
950
1384
|
where,
|
|
951
1385
|
"invocation references an existing instance and cannot create it again",
|
|
952
1386
|
);
|
|
953
|
-
|
|
1387
|
+
const targetInst = byId.get(targetId);
|
|
1388
|
+
if (!targetInst?.actions[call.action])
|
|
954
1389
|
add(where, "invocation target must name a declared action");
|
|
1390
|
+
else if (!call.guard) {
|
|
1391
|
+
const targetAction = targetInst.actions[call.action]!;
|
|
1392
|
+
if (
|
|
1393
|
+
inst.subject &&
|
|
1394
|
+
targetInst.subject &&
|
|
1395
|
+
inst.subject === targetInst.subject
|
|
1396
|
+
) {
|
|
1397
|
+
const callerRequirements = action.subject?.requirements ?? [];
|
|
1398
|
+
for (const targetReq of targetAction.subject?.requirements ??
|
|
1399
|
+
[]) {
|
|
1400
|
+
const targetObjFieldName =
|
|
1401
|
+
targetReq.objectField ?? targetReq.field.name;
|
|
1402
|
+
const satisfied = callerRequirements.some(
|
|
1403
|
+
(cr) =>
|
|
1404
|
+
(cr.objectField ?? cr.field.name) === targetObjFieldName,
|
|
1405
|
+
);
|
|
1406
|
+
if (!satisfied) {
|
|
1407
|
+
add(
|
|
1408
|
+
`${where}.invoke`,
|
|
1409
|
+
`mandatory invocation of ${targetId}.${call.action} requires subject field ${targetReq.field.name}`,
|
|
1410
|
+
"UDL2002",
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
955
1416
|
}
|
|
956
1417
|
targets.push({
|
|
957
1418
|
targets: ids.map((id) => `${id}.${call.action}`),
|