@hyperscale0/udl 4.4.0 → 4.5.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.
Files changed (48) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +1 -1
  3. package/dist/canonical.d.ts.map +1 -1
  4. package/dist/canonical.js +1 -29
  5. package/dist/canonical.js.map +1 -1
  6. package/dist/diagnostics.d.ts.map +1 -1
  7. package/dist/diagnostics.js +9 -6
  8. package/dist/diagnostics.js.map +1 -1
  9. package/dist/evolution.d.ts.map +1 -1
  10. package/dist/evolution.js +6 -16
  11. package/dist/evolution.js.map +1 -1
  12. package/dist/json.d.ts +2 -0
  13. package/dist/json.d.ts.map +1 -0
  14. package/dist/json.js +30 -0
  15. package/dist/json.js.map +1 -0
  16. package/dist/limits.d.ts +3 -30
  17. package/dist/limits.d.ts.map +1 -1
  18. package/dist/limits.js +4 -31
  19. package/dist/limits.js.map +1 -1
  20. package/dist/object-contract.d.ts +1 -0
  21. package/dist/object-contract.d.ts.map +1 -1
  22. package/dist/object-contract.js +4 -1
  23. package/dist/object-contract.js.map +1 -1
  24. package/dist/parser.d.ts.map +1 -1
  25. package/dist/parser.js +23 -0
  26. package/dist/parser.js.map +1 -1
  27. package/dist/reporting-validation.d.ts.map +1 -1
  28. package/dist/reporting-validation.js +12 -5
  29. package/dist/reporting-validation.js.map +1 -1
  30. package/dist/schema.d.ts.map +1 -1
  31. package/dist/schema.js +3 -4
  32. package/dist/schema.js.map +1 -1
  33. package/dist/validation.d.ts.map +1 -1
  34. package/dist/validation.js +157 -43
  35. package/dist/validation.js.map +1 -1
  36. package/docs/README.md +48 -32
  37. package/package.json +2 -2
  38. package/spec/README.md +48 -32
  39. package/src/canonical.ts +1 -35
  40. package/src/diagnostics.ts +9 -7
  41. package/src/evolution.ts +8 -18
  42. package/src/json.ts +34 -0
  43. package/src/limits.ts +4 -31
  44. package/src/object-contract.ts +6 -1
  45. package/src/parser.ts +22 -0
  46. package/src/reporting-validation.ts +12 -10
  47. package/src/schema.ts +7 -10
  48. package/src/validation.ts +220 -47
package/src/validation.ts CHANGED
@@ -18,6 +18,8 @@ import {
18
18
  import { issue, type UdlIssue } from "./diagnostics.js";
19
19
  import { validateReportDefinition } from "./reporting-validation.js";
20
20
  import { analyzeInstrumentFinance } from "./finance.js";
21
+ import { UDL_LIMITS } from "./limits.js";
22
+ import { udlFieldValueSchema } from "./object-contract.js";
21
23
 
22
24
  export const RESERVED_OBJECT_NAMES = [
23
25
  "objectId",
@@ -33,7 +35,7 @@ function hasParty(
33
35
  instrument: UdlInstrument | undefined,
34
36
  name: string,
35
37
  ): boolean {
36
- if (document.parties[name]) return true;
38
+ if (own(document.parties, name)) return true;
37
39
  if (!instrument?.subject) return false;
38
40
  return (
39
41
  document.objects
@@ -56,9 +58,9 @@ function moneyParty(
56
58
  ): boolean {
57
59
  return (
58
60
  hasParty(document, instrument, name) &&
59
- (document.parties[name]?.kind === "business" ||
60
- (!document.parties[name] && !!instrument?.subject) ||
61
- (!instrument?.subject && document.parties[name]?.kind === "person"))
61
+ (own(document.parties, name)?.kind === "business" ||
62
+ (!own(document.parties, name) && !!instrument?.subject) ||
63
+ (!instrument?.subject && own(document.parties, name)?.kind === "person"))
62
64
  );
63
65
  }
64
66
 
@@ -72,34 +74,73 @@ export class UdlError extends Error {
72
74
  }
73
75
  }
74
76
 
75
- /** Reject cycles and oversized structures before the schema walks caller data. */
77
+ /** Reject non-JSON properties before either the schema or serializer reads them. */
76
78
  function bounded(value: unknown): boolean {
77
79
  const active = new Set<object>();
80
+ const encoder = new TextEncoder();
78
81
  let nodes = 0;
79
82
  let bytes = 0;
83
+ const string = (value: string): boolean => {
84
+ if (value.length > UDL_LIMITS.maxTotalStringLength) return false;
85
+ bytes += encoder.encode(value).byteLength;
86
+ return bytes <= UDL_LIMITS.maxTotalStringLength;
87
+ };
80
88
  const visit = (v: unknown, depth: number): boolean => {
81
- if (++nodes > 100000 || depth > 32) return false;
82
- if (typeof v === "string") {
83
- bytes += v.length;
84
- return bytes <= 1048576;
85
- }
89
+ if (++nodes > UDL_LIMITS.maxNodes || depth > UDL_LIMITS.maxDepth)
90
+ return false;
91
+ if (typeof v === "string") return string(v);
86
92
  if (v === null || typeof v === "boolean") return true;
87
93
  if (typeof v === "number") return Number.isSafeInteger(v);
88
94
  if (typeof v !== "object" || active.has(v)) return false;
95
+ const array = Array.isArray(v);
89
96
  if (
90
- !Array.isArray(v) &&
97
+ !array &&
91
98
  Object.getPrototypeOf(v) !== Object.prototype &&
92
99
  Object.getPrototypeOf(v) !== null
93
100
  )
94
101
  return false;
102
+ const keys = Reflect.ownKeys(v);
103
+ if (
104
+ array &&
105
+ (v.length > UDL_LIMITS.maxNodes || keys.length !== v.length + 1)
106
+ )
107
+ return false;
95
108
  active.add(v);
96
- const ok = Object.entries(v).every(
97
- ([key, entry]) => key.length <= 240 && visit(entry, depth + 1),
98
- );
109
+ for (const key of keys) {
110
+ if (array && key === "length") continue;
111
+ if (
112
+ typeof key !== "string" ||
113
+ key === "__proto__" ||
114
+ key.length > UDL_LIMITS.maxKeyLength ||
115
+ !string(key)
116
+ )
117
+ return false;
118
+ if (array && (!/^(0|[1-9][0-9]*)$/.test(key) || Number(key) >= v.length))
119
+ return false;
120
+ const descriptor = Object.getOwnPropertyDescriptor(v, key);
121
+ if (
122
+ !descriptor?.enumerable ||
123
+ !("value" in descriptor) ||
124
+ !visit(descriptor.value, depth + 1)
125
+ )
126
+ return false;
127
+ }
99
128
  active.delete(v);
100
- return ok;
129
+ return true;
101
130
  };
102
- return visit(value, 0);
131
+ try {
132
+ return visit(value, 0);
133
+ } catch {
134
+ // Proxies may throw during reflection. They are not decoded JSON.
135
+ return false;
136
+ }
137
+ }
138
+
139
+ function own<T>(
140
+ record: Record<string, T> | undefined,
141
+ key: string,
142
+ ): T | undefined {
143
+ return record && Object.hasOwn(record, key) ? record[key] : undefined;
103
144
  }
104
145
 
105
146
  const targetIds = (target: string | string[]): string[] =>
@@ -422,7 +463,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
422
463
  );
423
464
 
424
465
  for (const name of subjectPartyRoles)
425
- if (document.parties[name])
466
+ if (own(document.parties, name))
426
467
  add(
427
468
  `$.parties.${name}`,
428
469
  `${name} is a reserved subject role`,
@@ -503,6 +544,17 @@ export function validateUdl(value: unknown): UdlValidationResult {
503
544
  }
504
545
  }
505
546
  }
547
+ if (f.type === "text" && f.value !== undefined) {
548
+ try {
549
+ if (
550
+ !udlFieldValueSchema({ ...f, value: undefined }).safeParse(f.value)
551
+ .success
552
+ )
553
+ add(where, `${f.name} constant violates its text constraints`);
554
+ } catch {
555
+ // Invalid character classes already have a diagnostic above.
556
+ }
557
+ }
506
558
  if (f.type === "ref") {
507
559
  duplicate(targetIds(f.target), where);
508
560
  for (const id of targetIds(f.target)) {
@@ -613,7 +665,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
613
665
  );
614
666
  for (const [attachmentIndex, attachment] of obj.attachments.entries()) {
615
667
  for (const [parameter, binding] of Object.entries(attachment.parties))
616
- if ("party" in binding && !document.parties[binding.party])
668
+ if ("party" in binding && !own(document.parties, binding.party))
617
669
  add(
618
670
  `${base}.attachments[${attachmentIndex}].parties.${parameter}`,
619
671
  `unknown declared party ${binding.party}`,
@@ -666,10 +718,10 @@ export function validateUdl(value: unknown): UdlValidationResult {
666
718
 
667
719
  const reportIds = new Set<string>();
668
720
  for (const [index, instrument] of document.instruments.entries()) {
669
- for (const report of instrument.reports ?? []) {
670
- const path = `$.instruments[${index}].reports`;
721
+ for (const [reportIndex, report] of (instrument.reports ?? []).entries()) {
722
+ const path = `$.instruments[${index}].reports[${reportIndex}]`;
671
723
  if (reportIds.has(report.identity.id))
672
- add(path, `duplicate report ${report.identity.id}`);
724
+ add(path, `duplicate report ${report.identity.id}`, "UDL2001");
673
725
  reportIds.add(report.identity.id);
674
726
  try {
675
727
  validateReportDefinition(report, document);
@@ -687,6 +739,18 @@ export function validateUdl(value: unknown): UdlValidationResult {
687
739
  const base = `$.instruments[${index}]`;
688
740
  if (inst.subject && !byObjectId.has(inst.subject))
689
741
  add(base, `unknown subject object ${inst.subject}`);
742
+ if (
743
+ inst.subject &&
744
+ Object.values(inst.actions).some((action) => action.publicAction) &&
745
+ !byObjectId
746
+ .get(inst.subject)
747
+ ?.attachments.some((attachment) => attachment.instrument === inst.id)
748
+ )
749
+ add(
750
+ `${base}.subject`,
751
+ "public subject actions require an object attachment",
752
+ "UDL5001",
753
+ );
690
754
  const field = (
691
755
  p: string,
692
756
  input: readonly UdlField[] = [],
@@ -731,12 +795,34 @@ export function validateUdl(value: unknown): UdlValidationResult {
731
795
  : type === "integer" || type === "percent" || type === "duration"
732
796
  ? typeof v.literal === "number" &&
733
797
  Number.isSafeInteger(v.literal) &&
734
- (type !== "percent" || (v.literal >= 0 && v.literal <= 10000))
735
- : type === "boolean"
736
- ? typeof v.literal === "boolean"
737
- : typeof v.literal === "string";
798
+ (type !== "percent" || (v.literal >= 0 && v.literal <= 10000)) &&
799
+ (type !== "duration" || v.literal > 0)
800
+ : type === "date"
801
+ ? udlFieldValueSchema({
802
+ name: "literal",
803
+ type: "date",
804
+ }).safeParse(v.literal).success
805
+ : type === "boolean"
806
+ ? typeof v.literal === "boolean"
807
+ : typeof v.literal === "string";
738
808
  if (!valid) add(where, `literal must have type ${type}`);
739
809
  };
810
+ const checkAssignment = (
811
+ value: UdlValue,
812
+ target: UdlField,
813
+ where: string,
814
+ action: UdlAction,
815
+ ) => {
816
+ checkValue(value, target.type, where, action.input, action);
817
+ if ("literal" in value) {
818
+ try {
819
+ if (!udlFieldValueSchema(target).safeParse(value.literal).success)
820
+ add(where, `literal violates constraints of ${target.name}`);
821
+ } catch {
822
+ // Invalid field constraints are reported by checkFields.
823
+ }
824
+ }
825
+ };
740
826
  checkFields(inst.fields, `${base}.fields`, false, inst);
741
827
  const captures = new Set(
742
828
  Object.values(inst.actions).flatMap((a) => [
@@ -856,7 +942,8 @@ export function validateUdl(value: unknown): UdlValidationResult {
856
942
  where,
857
943
  );
858
944
  const dependencies = new Map<string, string[]>();
859
- for (const c of calculations) {
945
+ for (const [calculationIndex, c] of calculations.entries()) {
946
+ const calculationPath = `${where}[${calculationIndex}]`;
860
947
  const arithmetic = ["sum", "subtract", "minimum", "divide"].includes(
861
948
  c.op,
862
949
  );
@@ -881,7 +968,18 @@ export function validateUdl(value: unknown): UdlValidationResult {
881
968
  checkValue(v, arithmetic ? resultType : "money", where, input, act);
882
969
  };
883
970
  if (c.op === "at") {
884
- expect(c.list, "list", where, input, act);
971
+ const list = expect(c.list, "list", where, input, act);
972
+ if (
973
+ result?.type === "ref" &&
974
+ list?.type === "list" &&
975
+ (result.targetKind !== list.targetKind ||
976
+ result.target !== list.target)
977
+ )
978
+ add(
979
+ calculationPath,
980
+ "list extraction requires the same reference target",
981
+ "UDL5001",
982
+ );
885
983
  checkValue(c.position, "integer", where, input, act);
886
984
  if ("literal" in c.position && Number(c.position.literal) < 1)
887
985
  add(where, "list positions start at one");
@@ -889,6 +987,10 @@ export function validateUdl(value: unknown): UdlValidationResult {
889
987
  }
890
988
  if (c.op === "aggregate") {
891
989
  const selected = checkSelection(c.selection, where, input);
990
+ operands.push(
991
+ { field: c.selection.anchor },
992
+ ...Object.values(c.selection.where ?? {}),
993
+ );
892
994
  if (
893
995
  c.measure !== "count" &&
894
996
  (!selected ||
@@ -911,11 +1013,24 @@ export function validateUdl(value: unknown): UdlValidationResult {
911
1013
  checkValue(c.numerator, type, where, input, act);
912
1014
  checkValue(c.denominator, type, where, input, act);
913
1015
  }
914
- if (
915
- "literal" in c.denominator &&
916
- BigInt(c.denominator.literal.toString()) <= 0n
917
- )
918
- add(where, "ratio denominator must be positive");
1016
+ for (const [operand, positive] of [
1017
+ [c.numerator, false],
1018
+ [c.denominator, true],
1019
+ ] as const) {
1020
+ if (
1021
+ "literal" in operand &&
1022
+ (typeof operand.literal === "number" ||
1023
+ (typeof operand.literal === "string" &&
1024
+ /^(0|[1-9][0-9]{0,17})$/.test(operand.literal))) &&
1025
+ (positive
1026
+ ? BigInt(operand.literal) <= 0n
1027
+ : BigInt(operand.literal) < 0n)
1028
+ )
1029
+ add(
1030
+ calculationPath,
1031
+ "ratio numerator must be nonnegative and denominator positive",
1032
+ );
1033
+ }
919
1034
  operands.push(c.numerator, c.denominator);
920
1035
  }
921
1036
  if (c.op === "sum" || c.op === "minimum") c.values.forEach(money);
@@ -946,7 +1061,19 @@ export function validateUdl(value: unknown): UdlValidationResult {
946
1061
  }
947
1062
  if (c.op === "shift") {
948
1063
  checkValue(c.date, "date", where, input, act);
949
- checkValue(c.milliseconds, "duration", where, input, act);
1064
+ checkValue(
1065
+ c.milliseconds,
1066
+ "literal" in c.milliseconds ? "integer" : "duration",
1067
+ where,
1068
+ input,
1069
+ act,
1070
+ );
1071
+ if (
1072
+ "literal" in c.milliseconds &&
1073
+ typeof c.milliseconds.literal === "number" &&
1074
+ c.milliseconds.literal < 0
1075
+ )
1076
+ add(where, "shift milliseconds must be nonnegative");
950
1077
  operands.push(c.date, c.milliseconds);
951
1078
  }
952
1079
  dependencies.set(
@@ -983,13 +1110,13 @@ export function validateUdl(value: unknown): UdlValidationResult {
983
1110
  duplicate(inst.actionOrder, `${base}.actionOrder`);
984
1111
  if (
985
1112
  inst.actionOrder.length !== Object.keys(inst.actions).length ||
986
- inst.actionOrder.some((a) => !inst.actions[a])
1113
+ inst.actionOrder.some((a) => !own(inst.actions, a))
987
1114
  )
988
1115
  add(base, "actionOrder must list every action once");
989
1116
  const reachable = new Set([inst.lifecycle.initial]);
990
1117
  for (const [action, edge] of Object.entries(inst.lifecycle.transitions)) {
991
1118
  if (
992
- !inst.actions[action] ||
1119
+ !own(inst.actions, action) ||
993
1120
  action === "create" ||
994
1121
  (edge.to !== "preserve" && !states.has(edge.to)) ||
995
1122
  edge.from.some((s) => !states.has(s))
@@ -1050,6 +1177,12 @@ export function validateUdl(value: unknown): UdlValidationResult {
1050
1177
  }
1051
1178
  } else if (req.kind === "state") {
1052
1179
  const target = expect(req.reference, "ref", where, input, act);
1180
+ if (target?.type === "ref" && target.targetKind !== "instrument")
1181
+ add(
1182
+ where,
1183
+ "state gates require an instrument reference",
1184
+ "UDL5001",
1185
+ );
1053
1186
  if (
1054
1187
  target?.type === "ref" &&
1055
1188
  target.targetKind === "instrument" &&
@@ -1115,7 +1248,10 @@ export function validateUdl(value: unknown): UdlValidationResult {
1115
1248
  checkRequirements(inst.invariants, `${base}.invariants`);
1116
1249
  for (const [actionName, action] of Object.entries(inst.actions)) {
1117
1250
  const where = `${base}.actions.${actionName}`;
1118
- if (actionName !== "create" && !inst.lifecycle.transitions[actionName])
1251
+ if (
1252
+ actionName !== "create" &&
1253
+ !own(inst.lifecycle.transitions, actionName)
1254
+ )
1119
1255
  add(where, "action needs a lifecycle transition", "UDL3001");
1120
1256
  if (action.subject) {
1121
1257
  const subBase = `${where}.subject`;
@@ -1127,7 +1263,25 @@ export function validateUdl(value: unknown): UdlValidationResult {
1127
1263
  `${subBase}.requirements`,
1128
1264
  true,
1129
1265
  );
1130
- for (const req of action.subject.requirements) {
1266
+ duplicate(
1267
+ action.subject.requirements.map(
1268
+ (requirement) => requirement.field.name,
1269
+ ),
1270
+ `${subBase}.requirements`,
1271
+ );
1272
+ for (const [
1273
+ requirementIndex,
1274
+ req,
1275
+ ] of action.subject.requirements.entries()) {
1276
+ for (const condition of (req.when ?? []).flat()) {
1277
+ const target = byId.get(condition.instrument);
1278
+ if (!own(target?.actions, condition.action))
1279
+ add(
1280
+ `${subBase}.requirements[${requirementIndex}].when`,
1281
+ "subject condition needs a declared instrument action",
1282
+ "UDL5001",
1283
+ );
1284
+ }
1131
1285
  if (req.field.optional)
1132
1286
  add(
1133
1287
  `${subBase}.requirements`,
@@ -1162,7 +1316,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
1162
1316
  );
1163
1317
  for (const reqField of adapter.snapshot.requirements) {
1164
1318
  const objectName =
1165
- adapter.renames?.[reqField.name] ?? reqField.name;
1319
+ own(adapter.renames, reqField.name) ?? reqField.name;
1166
1320
  const requirement = action.subject.requirements.find(
1167
1321
  (item) =>
1168
1322
  (item.objectField ?? item.field.name) === objectName &&
@@ -1293,13 +1447,32 @@ export function validateUdl(value: unknown): UdlValidationResult {
1293
1447
  from.book !== to.book
1294
1448
  )
1295
1449
  add(where, "moves cannot cross account books", "UDL4001");
1450
+ const accountOwner = (
1451
+ field: Extract<UdlField, { type: "account" }>,
1452
+ path: string,
1453
+ ): string => {
1454
+ if (typeof field.owner === "string") return `party:${field.owner}`;
1455
+ const binding = field.owner.adapter;
1456
+ const provider =
1457
+ path === `self.${field.name}`
1458
+ ? Object.values(inst.actions)
1459
+ .flatMap((action) => action.subject?.adapters ?? [])
1460
+ .find(
1461
+ (entry) => entry.binding === binding && entry.snapshot,
1462
+ )?.snapshot?.provider
1463
+ : undefined;
1464
+ return provider ? `provider:${provider}` : `binding:${binding}`;
1465
+ };
1296
1466
  const sameBoundAccount =
1297
1467
  from?.type === "account" &&
1298
1468
  to?.type === "account" &&
1299
- from.owner !== "self" &&
1300
- JSON.stringify(from.owner) === JSON.stringify(to.owner) &&
1469
+ (from.owner !== "self" ||
1470
+ (move.from === `self.${from.name}` &&
1471
+ move.to === `self.${to.name}`)) &&
1472
+ accountOwner(from, move.from) === accountOwner(to, move.to) &&
1301
1473
  from.book === to.book &&
1302
- (from.key ?? "balance") === (to.key ?? "balance");
1474
+ (from.key ?? (from.owner === "self" ? from.name : "balance")) ===
1475
+ (to.key ?? (to.owner === "self" ? to.name : "balance"));
1303
1476
  if (move.from === move.to || sameBoundAccount)
1304
1477
  add(where, "a transfer needs distinct accounts", "UDL4001");
1305
1478
  } else expect(move.transfer, "text", where, action.input, action);
@@ -1325,7 +1498,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
1325
1498
  inst.calculate.some((c) => c.target === key)
1326
1499
  )
1327
1500
  add(where, `cannot write immutable field ${key}`);
1328
- else checkValue(v, target.type, where, action.input, action);
1501
+ else checkAssignment(v, target, `${where}.set.${key}`, action);
1329
1502
  }
1330
1503
  const checkArguments = (
1331
1504
  targetId: string | undefined,
@@ -1334,14 +1507,14 @@ export function validateUdl(value: unknown): UdlValidationResult {
1334
1507
  binding?: string,
1335
1508
  ) => {
1336
1509
  const target = targetId
1337
- ? byId.get(targetId)?.actions[actionName]
1510
+ ? own(byId.get(targetId)?.actions, actionName)
1338
1511
  : undefined;
1339
1512
  if (!target) return;
1340
1513
  for (const required of target.input)
1341
1514
  if (
1342
1515
  !required.optional &&
1343
1516
  !("value" in required && required.value !== undefined) &&
1344
- !(required.name in values)
1517
+ !Object.hasOwn(values, required.name)
1345
1518
  )
1346
1519
  add(where, `missing target input ${required.name}`);
1347
1520
  for (const [name, value] of Object.entries(values)) {
@@ -1355,7 +1528,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
1355
1528
  add(where, "range binding requires an integer target input");
1356
1529
  continue;
1357
1530
  }
1358
- checkValue(value, declared.type, where, action.input, action);
1531
+ checkAssignment(value, declared, where, action);
1359
1532
  if (declared.type === "ref" && "field" in value) {
1360
1533
  const supplied = field(value.field, action.input, action);
1361
1534
  if (
@@ -1414,10 +1587,10 @@ export function validateUdl(value: unknown): UdlValidationResult {
1414
1587
  "invocation references an existing instance and cannot create it again",
1415
1588
  );
1416
1589
  const targetInst = byId.get(targetId);
1417
- if (!targetInst?.actions[call.action])
1590
+ const targetAction = own(targetInst?.actions, call.action);
1591
+ if (!targetInst || !targetAction)
1418
1592
  add(where, "invocation target must name a declared action");
1419
1593
  else if (!call.guard) {
1420
- const targetAction = targetInst.actions[call.action]!;
1421
1594
  if (
1422
1595
  inst.subject &&
1423
1596
  targetInst.subject &&