@hyperscale0/udl 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/dist/finance.js +2 -1
- package/dist/finance.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- 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 +1104 -3
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +60 -8
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +89 -12
- package/dist/validation.js.map +1 -1
- package/docs/README.md +26 -0
- package/package.json +2 -2
- package/spec/README.md +26 -0
- package/spec/udl.schema.json +1960 -4
- package/src/finance.ts +1 -1
- package/src/index.ts +6 -0
- package/src/reporting-validation.ts +381 -0
- package/src/reporting.ts +179 -0
- package/src/schema.ts +61 -8
- package/src/validation.ts +100 -10
package/src/schema.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
|
+
import { reportDefinitionSchema } from "./reporting.js";
|
|
2
3
|
|
|
3
4
|
export const UDL_FORMAT_VERSION = 3 as const;
|
|
5
|
+
/** Counts the root and every nested invocation, including selected and ranged children. */
|
|
6
|
+
export const MAX_ACTION_EXPANSION = 4096;
|
|
4
7
|
const name = z
|
|
5
8
|
.string()
|
|
6
9
|
.regex(/^[a-z][a-zA-Z0-9_]*$/)
|
|
@@ -62,7 +65,13 @@ export const udlFieldSchema = z.discriminatedUnion("type", [
|
|
|
62
65
|
...fieldBase,
|
|
63
66
|
type: z.literal("text"),
|
|
64
67
|
value: text.optional(),
|
|
68
|
+
minLength: integer.positive().max(2048).optional(),
|
|
65
69
|
maxLength: integer.positive().max(2048).optional(),
|
|
70
|
+
// One anchored ASCII character class and a fixed repetition. No executable patterns.
|
|
71
|
+
pattern: z
|
|
72
|
+
.string()
|
|
73
|
+
.regex(/^\^\[[A-Za-z0-9-]+\]\{[1-9][0-9]{0,3}\}\$$/)
|
|
74
|
+
.optional(),
|
|
66
75
|
}),
|
|
67
76
|
z.strictObject({
|
|
68
77
|
...fieldBase,
|
|
@@ -183,6 +192,13 @@ export const udlCalculationSchema = z.discriminatedUnion("op", [
|
|
|
183
192
|
}),
|
|
184
193
|
]);
|
|
185
194
|
|
|
195
|
+
const comparisonRequirement = z.strictObject({
|
|
196
|
+
kind: z.literal("compare"),
|
|
197
|
+
left: value,
|
|
198
|
+
operator: z.enum(["==", "!=", "<", "<=", ">", ">="]),
|
|
199
|
+
right: value,
|
|
200
|
+
});
|
|
201
|
+
|
|
186
202
|
export const udlRequirementSchema = z.discriminatedUnion("kind", [
|
|
187
203
|
z.strictObject({
|
|
188
204
|
kind: z.literal("hours"),
|
|
@@ -191,12 +207,7 @@ export const udlRequirementSchema = z.discriminatedUnion("kind", [
|
|
|
191
207
|
end: integer.min(0).max(23),
|
|
192
208
|
timezone: text,
|
|
193
209
|
}),
|
|
194
|
-
|
|
195
|
-
kind: z.literal("compare"),
|
|
196
|
-
left: value,
|
|
197
|
-
operator: z.enum(["==", "!=", "<", "<=", ">", ">="]),
|
|
198
|
-
right: value,
|
|
199
|
-
}),
|
|
210
|
+
comparisonRequirement,
|
|
200
211
|
z.strictObject({
|
|
201
212
|
kind: z.literal("state"),
|
|
202
213
|
reference: path,
|
|
@@ -267,16 +278,46 @@ export const udlMoveSchema = z.discriminatedUnion("operation", [
|
|
|
267
278
|
transfer: path,
|
|
268
279
|
}),
|
|
269
280
|
]);
|
|
270
|
-
const clock = z.strictObject({
|
|
281
|
+
const clock = z.strictObject({
|
|
282
|
+
at: path,
|
|
283
|
+
offset: integer.optional(),
|
|
284
|
+
localDay: z
|
|
285
|
+
.strictObject({
|
|
286
|
+
days: integer.min(0).max(366),
|
|
287
|
+
direction: z.enum(["before", "after"]),
|
|
288
|
+
hour: integer.min(0).max(23),
|
|
289
|
+
timezone: text,
|
|
290
|
+
})
|
|
291
|
+
.optional(),
|
|
292
|
+
});
|
|
271
293
|
export const udlActionSchema = z.strictObject({
|
|
272
294
|
summary: text,
|
|
273
295
|
publicAction: name.optional(),
|
|
296
|
+
expansionLimit: z.literal(8192).optional(),
|
|
297
|
+
humanApproval: z.literal("distinct_member").optional(),
|
|
298
|
+
reminder: z
|
|
299
|
+
.strictObject({
|
|
300
|
+
installment: path,
|
|
301
|
+
recipient: path,
|
|
302
|
+
dueAt: path,
|
|
303
|
+
channel: z.literal("email"),
|
|
304
|
+
template: z.literal("payment_reminder"),
|
|
305
|
+
beforeDays: integer.min(0).max(366),
|
|
306
|
+
overdueDays: integer.min(1).max(366),
|
|
307
|
+
maxPerDay: integer.min(1).max(10),
|
|
308
|
+
startHour: integer.min(0).max(23),
|
|
309
|
+
endHour: integer.min(1).max(24),
|
|
310
|
+
timezone: text,
|
|
311
|
+
})
|
|
312
|
+
.optional(),
|
|
274
313
|
event: text,
|
|
275
314
|
actor: z.union([
|
|
276
315
|
z.literal("caller"),
|
|
277
316
|
z.literal("clock"),
|
|
278
317
|
z.strictObject({ party: name }),
|
|
279
|
-
z.strictObject({
|
|
318
|
+
z.strictObject({
|
|
319
|
+
parent: z.union([instrumentId, z.array(instrumentId).min(1).max(16)]),
|
|
320
|
+
}),
|
|
280
321
|
]),
|
|
281
322
|
input: z.array(udlFieldSchema).max(128),
|
|
282
323
|
requires: z.array(udlRequirementSchema).max(128),
|
|
@@ -292,16 +333,26 @@ export const udlActionSchema = z.strictObject({
|
|
|
292
333
|
instrument: instrumentId,
|
|
293
334
|
action: z.literal("create"),
|
|
294
335
|
input: z.record(name, value),
|
|
336
|
+
guard: comparisonRequirement.optional(),
|
|
337
|
+
range: z
|
|
338
|
+
.strictObject({
|
|
339
|
+
count: value,
|
|
340
|
+
maximum: integer.min(1).max(366),
|
|
341
|
+
bind: name,
|
|
342
|
+
})
|
|
343
|
+
.optional(),
|
|
295
344
|
}),
|
|
296
345
|
z.strictObject({
|
|
297
346
|
reference: path,
|
|
298
347
|
action: name,
|
|
299
348
|
input: z.record(name, value),
|
|
349
|
+
guard: comparisonRequirement.optional(),
|
|
300
350
|
}),
|
|
301
351
|
z.strictObject({
|
|
302
352
|
selection,
|
|
303
353
|
action: name,
|
|
304
354
|
input: z.record(name, value),
|
|
355
|
+
guard: comparisonRequirement.optional(),
|
|
305
356
|
}),
|
|
306
357
|
]),
|
|
307
358
|
)
|
|
@@ -327,6 +378,8 @@ export const udlLifecycleSchema = z.strictObject({
|
|
|
327
378
|
),
|
|
328
379
|
});
|
|
329
380
|
export const udlInstrumentSchema = z.strictObject({
|
|
381
|
+
reports: z.array(reportDefinitionSchema).max(16).optional(),
|
|
382
|
+
revisioned: z.literal(true).optional(),
|
|
330
383
|
id: instrumentId,
|
|
331
384
|
title: text,
|
|
332
385
|
summary: text,
|
package/src/validation.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
udlDocumentSchema,
|
|
3
|
+
MAX_ACTION_EXPANSION,
|
|
3
4
|
type UdlDocument,
|
|
4
5
|
type UdlField,
|
|
5
6
|
type UdlInstrument,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
type UdlSelection,
|
|
9
10
|
} from "./schema.js";
|
|
10
11
|
import { issue, type UdlIssue } from "./diagnostics.js";
|
|
12
|
+
import { validateReportDefinition } from "./reporting-validation.js";
|
|
11
13
|
import { analyzeInstrumentFinance } from "./finance.js";
|
|
12
14
|
|
|
13
15
|
export type UdlValidationResult =
|
|
@@ -149,6 +151,7 @@ function resolvePath(
|
|
|
149
151
|
if (key === "id") return { name: key, type: "ref", target: instrument.id };
|
|
150
152
|
if (key === "now" || key === "createdAt")
|
|
151
153
|
return { name: key, type: "date" };
|
|
154
|
+
if (key === "productRevision") return { name: key, type: "text" };
|
|
152
155
|
if (key === "status")
|
|
153
156
|
return { name: key, type: "enum", values: instrument.lifecycle.states };
|
|
154
157
|
}
|
|
@@ -239,6 +242,23 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
239
242
|
document.instruments.map((i) => i.id),
|
|
240
243
|
"$.instruments",
|
|
241
244
|
);
|
|
245
|
+
const reportIds = new Set<string>();
|
|
246
|
+
for (const [index, instrument] of document.instruments.entries()) {
|
|
247
|
+
for (const report of instrument.reports ?? []) {
|
|
248
|
+
const path = `$.instruments[${index}].reports`;
|
|
249
|
+
if (reportIds.has(report.identity.id))
|
|
250
|
+
add(path, `duplicate report ${report.identity.id}`);
|
|
251
|
+
reportIds.add(report.identity.id);
|
|
252
|
+
try {
|
|
253
|
+
validateReportDefinition(report, document);
|
|
254
|
+
} catch (error) {
|
|
255
|
+
add(
|
|
256
|
+
path,
|
|
257
|
+
error instanceof Error ? error.message : "Invalid report definition",
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
242
262
|
const byId = new Map(document.instruments.map((i) => [i.id, i]));
|
|
243
263
|
const calls = new Map<string, { targets: string[]; count: number }[]>();
|
|
244
264
|
for (const [index, inst] of document.instruments.entries()) {
|
|
@@ -294,8 +314,23 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
294
314
|
where,
|
|
295
315
|
"contra accounts require the claim book; external accounts require a cash party owner",
|
|
296
316
|
);
|
|
297
|
-
if (
|
|
317
|
+
if (
|
|
318
|
+
["id", "status", "createdAt", "now", "productRevision"].includes(
|
|
319
|
+
f.name,
|
|
320
|
+
)
|
|
321
|
+
)
|
|
298
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
|
+
}
|
|
299
334
|
if (f.type === "ref") {
|
|
300
335
|
duplicate(targetIds(f.target), where);
|
|
301
336
|
for (const id of targetIds(f.target))
|
|
@@ -561,6 +596,8 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
561
596
|
};
|
|
562
597
|
checkCalculations(inst.calculate, `${base}.calculate`);
|
|
563
598
|
const states = new Set(inst.lifecycle.states);
|
|
599
|
+
if (states.has("preserve"))
|
|
600
|
+
add(base, "preserve is a reserved transition destination", "UDL3001");
|
|
564
601
|
duplicate(inst.lifecycle.states, `${base}.lifecycle.states`);
|
|
565
602
|
if (!states.has(inst.lifecycle.initial) || !inst.actions.create)
|
|
566
603
|
add(base, "declare create and a declared initial state", "UDL3001");
|
|
@@ -575,14 +612,15 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
575
612
|
if (
|
|
576
613
|
!inst.actions[action] ||
|
|
577
614
|
action === "create" ||
|
|
578
|
-
!states.has(edge.to) ||
|
|
615
|
+
(edge.to !== "preserve" && !states.has(edge.to)) ||
|
|
579
616
|
edge.from.some((s) => !states.has(s))
|
|
580
617
|
)
|
|
581
618
|
add(base, `invalid transition ${action}`, "UDL3001");
|
|
582
619
|
}
|
|
583
620
|
for (let pass = 0; pass < states.size; pass++)
|
|
584
621
|
for (const edge of Object.values(inst.lifecycle.transitions))
|
|
585
|
-
if (edge.from.some((s) => reachable.has(s)))
|
|
622
|
+
if (edge.to !== "preserve" && edge.from.some((s) => reachable.has(s)))
|
|
623
|
+
reachable.add(edge.to);
|
|
586
624
|
for (const state of states)
|
|
587
625
|
if (!reachable.has(state))
|
|
588
626
|
add(base, `unreachable state ${state}`, "UDL3001");
|
|
@@ -696,6 +734,18 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
696
734
|
if (actionName !== "create" && !inst.lifecycle.transitions[actionName])
|
|
697
735
|
add(where, "action needs a lifecycle transition", "UDL3001");
|
|
698
736
|
checkFields(action.input, `${where}.input`);
|
|
737
|
+
if (action.reminder) {
|
|
738
|
+
expect(action.reminder.installment, "ref", where, action.input);
|
|
739
|
+
expect(action.reminder.recipient, "account", where, action.input);
|
|
740
|
+
expect(action.reminder.dueAt, "date", where, action.input);
|
|
741
|
+
if (action.reminder.startHour >= action.reminder.endHour)
|
|
742
|
+
add(where, "reminder sending window is empty");
|
|
743
|
+
try {
|
|
744
|
+
new Intl.DateTimeFormat("en", { timeZone: action.reminder.timezone });
|
|
745
|
+
} catch {
|
|
746
|
+
add(where, "reminder requires an IANA timezone");
|
|
747
|
+
}
|
|
748
|
+
}
|
|
699
749
|
for (const input of action.input) {
|
|
700
750
|
if (input.type === "account")
|
|
701
751
|
add(where, "account bindings cannot be caller inputs");
|
|
@@ -734,13 +784,24 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
734
784
|
if (
|
|
735
785
|
typeof action.actor === "object" &&
|
|
736
786
|
"parent" in action.actor &&
|
|
737
|
-
|
|
787
|
+
targetIds(action.actor.parent).some((id) => !byId.has(id))
|
|
738
788
|
)
|
|
739
789
|
add(where, "actor names an undeclared parent");
|
|
740
790
|
if (action.actor === "clock" && !action.due)
|
|
741
791
|
add(where, "clock action needs a due instant");
|
|
742
792
|
for (const clock of [action.due, action.deadline])
|
|
743
|
-
if (clock)
|
|
793
|
+
if (clock) {
|
|
794
|
+
expect(clock.at, "date", where, action.input);
|
|
795
|
+
if (clock.localDay) {
|
|
796
|
+
try {
|
|
797
|
+
new Intl.DateTimeFormat("en", {
|
|
798
|
+
timeZone: clock.localDay.timezone,
|
|
799
|
+
}).format(0);
|
|
800
|
+
} catch {
|
|
801
|
+
add(where, "clock timezone must be an IANA timezone");
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
744
805
|
for (const move of action.moves) {
|
|
745
806
|
if ("amount" in move) {
|
|
746
807
|
checkValue(move.amount, "money", where, action.input);
|
|
@@ -790,6 +851,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
790
851
|
targetId: string | undefined,
|
|
791
852
|
actionName: string,
|
|
792
853
|
values: Record<string, UdlValue>,
|
|
854
|
+
binding?: string,
|
|
793
855
|
) => {
|
|
794
856
|
const target = targetId
|
|
795
857
|
? byId.get(targetId)?.actions[actionName]
|
|
@@ -808,6 +870,11 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
808
870
|
add(where, `unknown target input ${name}`);
|
|
809
871
|
continue;
|
|
810
872
|
}
|
|
873
|
+
if (binding && "field" in value && value.field === binding) {
|
|
874
|
+
if (declared.type !== "integer")
|
|
875
|
+
add(where, "range binding requires an integer target input");
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
811
878
|
checkValue(value, declared.type, where, action.input);
|
|
812
879
|
if (declared.type === "ref" && "field" in value) {
|
|
813
880
|
const supplied = field(value.field, action.input);
|
|
@@ -844,6 +911,20 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
844
911
|
}
|
|
845
912
|
const targets: { targets: string[]; count: number }[] = [];
|
|
846
913
|
for (const call of action.invoke ?? []) {
|
|
914
|
+
if (call.guard) checkRequirements([call.guard], where, action.input);
|
|
915
|
+
const range = "instrument" in call ? call.range : undefined;
|
|
916
|
+
if (range) {
|
|
917
|
+
checkValue(range.count, "integer", where, action.input);
|
|
918
|
+
if (["self", "input", "party"].includes(range.bind))
|
|
919
|
+
add(where, "range binding cannot shadow a reserved scope");
|
|
920
|
+
if (
|
|
921
|
+
"literal" in range.count &&
|
|
922
|
+
(typeof range.count.literal !== "number" ||
|
|
923
|
+
range.count.literal < 1 ||
|
|
924
|
+
range.count.literal > range.maximum)
|
|
925
|
+
)
|
|
926
|
+
add(where, "range count must be between one and its maximum");
|
|
927
|
+
}
|
|
847
928
|
if ("selection" in call)
|
|
848
929
|
checkSelection(call.selection, where, action.input);
|
|
849
930
|
const target =
|
|
@@ -863,7 +944,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
863
944
|
: [];
|
|
864
945
|
if (!ids.length) add(where, "invocation needs a declared target");
|
|
865
946
|
for (const targetId of ids) {
|
|
866
|
-
checkArguments(targetId, call.action, call.input);
|
|
947
|
+
checkArguments(targetId, call.action, call.input, range?.bind);
|
|
867
948
|
if (call.action === "create" && !("instrument" in call))
|
|
868
949
|
add(
|
|
869
950
|
where,
|
|
@@ -874,7 +955,8 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
874
955
|
}
|
|
875
956
|
targets.push({
|
|
876
957
|
targets: ids.map((id) => `${id}.${call.action}`),
|
|
877
|
-
count:
|
|
958
|
+
count:
|
|
959
|
+
"selection" in call ? call.selection.limit : (range?.maximum ?? 1),
|
|
878
960
|
});
|
|
879
961
|
}
|
|
880
962
|
calls.set(`${inst.id}.${actionName}`, targets);
|
|
@@ -891,7 +973,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
891
973
|
const visit = (key: string): number => {
|
|
892
974
|
if (active.has(key)) {
|
|
893
975
|
add("$.instruments", `invocation cycle at ${key}`, "UDL2010");
|
|
894
|
-
return
|
|
976
|
+
return MAX_ACTION_EXPANSION + 1;
|
|
895
977
|
}
|
|
896
978
|
const known = depths.get(key);
|
|
897
979
|
if (known !== undefined) return known;
|
|
@@ -904,8 +986,16 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
904
986
|
);
|
|
905
987
|
active.delete(key);
|
|
906
988
|
depths.set(key, size);
|
|
907
|
-
|
|
908
|
-
|
|
989
|
+
const [id, action] = key.split(".");
|
|
990
|
+
const limit =
|
|
991
|
+
document.instruments.find((item) => item.id === id)?.actions[action!]
|
|
992
|
+
?.expansionLimit ?? MAX_ACTION_EXPANSION;
|
|
993
|
+
if (size > limit)
|
|
994
|
+
add(
|
|
995
|
+
"$.instruments",
|
|
996
|
+
`invocation ${key} exceeds ${limit} actions`,
|
|
997
|
+
"UDL2010",
|
|
998
|
+
);
|
|
909
999
|
return size;
|
|
910
1000
|
};
|
|
911
1001
|
for (const key of calls.keys()) visit(key);
|