@hyperscale0/udl 2.0.4 → 2.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +15 -1
  3. package/conformance/invalid/call-binds-results.expected.json +10 -0
  4. package/conformance/invalid/call-binds-results.udl +314 -0
  5. package/conformance/invalid/call-unknown-action.expected.json +10 -0
  6. package/conformance/invalid/call-unknown-action.udl +314 -0
  7. package/conformance/invalid/leaf-effect-mismatch.expected.json +10 -0
  8. package/conformance/invalid/leaf-effect-mismatch.udl +314 -0
  9. package/conformance/invalid/piece-plan-without-partition.expected.json +10 -0
  10. package/conformance/invalid/piece-plan-without-partition.udl +305 -0
  11. package/conformance/invalid/private-action-independent-approval.expected.json +10 -0
  12. package/conformance/invalid/private-action-independent-approval.udl +314 -0
  13. package/conformance/invalid/unfund-order-not-reversed.expected.json +10 -0
  14. package/conformance/invalid/unfund-order-not-reversed.udl +314 -0
  15. package/conformance/valid/piece-plan-calls.expected.json +6 -0
  16. package/conformance/valid/piece-plan-calls.udl +314 -0
  17. package/dist/diagnostics.d.ts +36 -0
  18. package/dist/diagnostics.d.ts.map +1 -1
  19. package/dist/diagnostics.js +36 -0
  20. package/dist/diagnostics.js.map +1 -1
  21. package/dist/effects.d.ts +24 -3
  22. package/dist/effects.d.ts.map +1 -1
  23. package/dist/effects.js +906 -8
  24. package/dist/effects.js.map +1 -1
  25. package/dist/index.d.ts +2 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/limits.d.ts +2 -0
  30. package/dist/limits.d.ts.map +1 -1
  31. package/dist/limits.js +2 -0
  32. package/dist/limits.js.map +1 -1
  33. package/dist/schema.d.ts +594 -11
  34. package/dist/schema.d.ts.map +1 -1
  35. package/dist/schema.js +111 -1
  36. package/dist/schema.js.map +1 -1
  37. package/dist/validation.d.ts.map +1 -1
  38. package/dist/validation.js +289 -4
  39. package/dist/validation.js.map +1 -1
  40. package/docs/assets/brand/manifest.json +30 -0
  41. package/docs/assets/brand/udl-horizontal-white.svg +1 -0
  42. package/docs/assets/brand/udl-horizontal.svg +1 -0
  43. package/docs/assets/brand/udl-stacked-white.svg +1 -0
  44. package/docs/assets/brand/udl-stacked.svg +1 -0
  45. package/docs/assets/udl.svg +7 -14
  46. package/docs/llms-full.txt +199 -32
  47. package/docs/llms.txt +1 -1
  48. package/docs/reference/clauses.md +162 -1
  49. package/docs/reference/cli.md +1 -1
  50. package/docs/reference/diagnostics.md +38 -32
  51. package/package.json +2 -2
  52. package/spec/udl.schema.json +321 -1
  53. package/src/diagnostics.ts +36 -0
  54. package/src/effects.ts +1603 -9
  55. package/src/index.ts +15 -0
  56. package/src/limits.ts +2 -0
  57. package/src/schema.ts +149 -2
  58. package/src/validation.ts +469 -4
@@ -4,7 +4,7 @@ import { udlClauseVocabulary, quoteExpiresAtRefKey, quoteFrozenRefKey, quoteSeed
4
4
  import { fixedIsoDurationMs } from "./duration.js";
5
5
  import { analyzeInstrumentFinance, financeAdmissionProblem, } from "./finance.js";
6
6
  import { UDL_LIMITS } from "./limits.js";
7
- import { deriveUdlActionEffects, udlEffectKinds } from "./effects.js";
7
+ import { deriveUdlActionEffects, resolveUdlActionPlans, udlEffectKinds, } from "./effects.js";
8
8
  import { issue } from "./diagnostics.js";
9
9
  export class UdlError extends Error {
10
10
  issues;
@@ -224,6 +224,7 @@ function semanticIssues(document, references, options) {
224
224
  }
225
225
  }
226
226
  for (const [instrumentIndex, instrument] of document.instruments.entries()) {
227
+ const resolvedPlansResult = resolveUdlActionPlans(instrument);
227
228
  for (const [actionName, action] of Object.entries(instrument.actions)) {
228
229
  if (options.requireDecisionPartyBindings) {
229
230
  for (const [index, role] of (action.port?.allowedParties ?? []).entries()) {
@@ -242,7 +243,14 @@ function semanticIssues(document, references, options) {
242
243
  }
243
244
  if (!action.effects)
244
245
  continue;
245
- const expected = deriveUdlActionEffects(action, udlClauseVocabulary);
246
+ let expected;
247
+ if (action.calls && action.calls.length > 0) {
248
+ const plan = resolvedPlansResult.plans.find((p) => p.action === actionName);
249
+ expected = plan ? plan.effects : {};
250
+ }
251
+ else {
252
+ expected = deriveUdlActionEffects(action, udlClauseVocabulary);
253
+ }
246
254
  for (const kind of udlEffectKinds) {
247
255
  const actualRows = action.effects[kind] ?? [];
248
256
  const expectedRows = expected[kind] ?? [];
@@ -885,11 +893,288 @@ function validateInstrument(instrument, instrumentIndex, instruments, subjects,
885
893
  validateSetsAt(instrument, base, add);
886
894
  validateActions(instrument, base, instruments, references, add);
887
895
  validateQuoteCommit(instrument, base, references, add);
888
- for (const issue of analyzeInstrumentFinance(instrument)) {
889
- add([...base, ...issue.path], issue.message, issue.code);
896
+ validatePiecePlan(instrument, base, references, add);
897
+ const planResolution = resolveUdlActionPlans(instrument);
898
+ for (const planIssue of planResolution.issues) {
899
+ const rawPath = planIssue.path.startsWith("$.")
900
+ ? planIssue.path.slice(2).split(".")
901
+ : [planIssue.path];
902
+ add([...base, ...rawPath], planIssue.message, planIssue.code);
903
+ }
904
+ // Validate leaf steps
905
+ const validatedLeaves = new Set();
906
+ for (const plan of planResolution.plans) {
907
+ const actionDef = instrument.actions[plan.action];
908
+ if (!actionDef)
909
+ continue;
910
+ for (const leaf of plan.leaves) {
911
+ const leafKey = `${plan.pieceId ?? ""}:${leaf.originPath.join(".")}`;
912
+ if (!validatedLeaves.has(leafKey)) {
913
+ validatedLeaves.add(leafKey);
914
+ validateStep(instrument, actionDef, leaf.step, [...base, "actions", ...leaf.originPath], add);
915
+ }
916
+ }
917
+ }
918
+ // Multi-variant finance oracle check across action plan combinations
919
+ const actionsWithPlans = Object.keys(instrument.actions).filter((aName) => planResolution.plans.some((p) => p.action === aName));
920
+ let totalCombinations = 1;
921
+ const actionPlanMap = {};
922
+ for (const aName of actionsWithPlans) {
923
+ const actionPlans = planResolution.plans.filter((p) => p.action === aName);
924
+ actionPlanMap[aName] = actionPlans;
925
+ totalCombinations *= actionPlans.length;
926
+ }
927
+ if (actionsWithPlans.length > 0 &&
928
+ totalCombinations > UDL_LIMITS.maxActionExpansion) {
929
+ add([...base, "actions"], `variant expansion exceeds combination bound of ${UDL_LIMITS.maxActionExpansion} (${totalCombinations} combinations)`, "UDL2010");
930
+ }
931
+ else {
932
+ const generateCombos = (keys) => {
933
+ if (keys.length === 0)
934
+ return [{}];
935
+ const [first, ...rest] = keys;
936
+ const restCombos = generateCombos(rest);
937
+ const result = [];
938
+ for (const plan of actionPlanMap[first]) {
939
+ for (const combo of restCombos) {
940
+ result.push({ ...combo, [first]: plan });
941
+ }
942
+ }
943
+ return result;
944
+ };
945
+ const combinations = actionsWithPlans.length > 0 ? generateCombos(actionsWithPlans) : [{}];
946
+ const seenFinanceIssues = new Set();
947
+ for (const combo of combinations) {
948
+ const expandedActions = {};
949
+ for (const [aName, aDef] of Object.entries(instrument.actions)) {
950
+ const plan = combo[aName];
951
+ // Only an action that moves money through calls is replaced by its
952
+ // expanded leaves. Authored moves and steps always reach the oracle.
953
+ if (plan && (aDef.calls?.length ?? 0) > 0) {
954
+ const steps = [];
955
+ const moves = [];
956
+ for (const leaf of plan.leaves) {
957
+ if ("key" in leaf.step) {
958
+ moves.push(leaf.step);
959
+ }
960
+ else {
961
+ steps.push(leaf.step);
962
+ }
963
+ }
964
+ expandedActions[aName] = {
965
+ ...aDef,
966
+ moves,
967
+ steps,
968
+ };
969
+ }
970
+ else {
971
+ expandedActions[aName] = aDef;
972
+ }
973
+ }
974
+ const financeInstrument = {
975
+ ...instrument,
976
+ actions: expandedActions,
977
+ };
978
+ for (const finIssue of analyzeInstrumentFinance(financeInstrument)) {
979
+ const issueKey = `${finIssue.code}:${finIssue.path.join(".")}:${finIssue.message}`;
980
+ if (!seenFinanceIssues.has(issueKey)) {
981
+ seenFinanceIssues.add(issueKey);
982
+ add([...base, ...finIssue.path], finIssue.message, finIssue.code);
983
+ }
984
+ }
985
+ }
890
986
  }
891
987
  validateAggregates(instrument, base, instruments, references, add);
892
988
  }
989
+ function validatePiecePlan(instrument, base, references, add) {
990
+ const plan = instrument.piecePlan;
991
+ if (!plan)
992
+ return;
993
+ const planBase = [...base, "piecePlan"];
994
+ const mutableFields = new Set(instrument.update?.fields ?? []);
995
+ const allUpdatedFields = new Set(Object.values(instrument.actions).flatMap((a) => a.updates ?? []));
996
+ // Validate total field
997
+ const totalSchema = instrument.fields[plan.total];
998
+ if (!totalSchema ||
999
+ totalSchema.type !== "string" ||
1000
+ !isMoneySchema(totalSchema)) {
1001
+ add([...planBase, "total"], `piece plan total ${plan.total} must be a declared money field`, "UDL4002");
1002
+ }
1003
+ if (!instrument.required.includes(plan.total)) {
1004
+ add([...planBase, "total"], `piece plan total ${plan.total} must be required`, "UDL4002");
1005
+ }
1006
+ if (mutableFields.has(plan.total) || allUpdatedFields.has(plan.total)) {
1007
+ add([...planBase, "total"], `piece plan total ${plan.total} cannot be updated`, "UDL4002");
1008
+ }
1009
+ const pieceIds = new Set();
1010
+ const pieceAmountFields = [];
1011
+ plan.pieces.forEach((piece, index) => {
1012
+ const pieceBase = [...planBase, "pieces", index];
1013
+ if (pieceIds.has(piece.id)) {
1014
+ add([...pieceBase, "id"], `duplicate piece id ${piece.id} in piece plan`, "UDL4002");
1015
+ }
1016
+ pieceIds.add(piece.id);
1017
+ pieceAmountFields.push(piece.amount);
1018
+ const amountSchema = instrument.fields[piece.amount];
1019
+ if (!amountSchema ||
1020
+ amountSchema.type !== "string" ||
1021
+ !isMoneySchema(amountSchema)) {
1022
+ add([...pieceBase, "amount"], `piece amount ${piece.amount} must be a declared money field`, "UDL4002");
1023
+ }
1024
+ if (!instrument.required.includes(piece.amount)) {
1025
+ add([...pieceBase, "amount"], `piece amount ${piece.amount} must be required`, "UDL4002");
1026
+ }
1027
+ if (mutableFields.has(piece.amount) || allUpdatedFields.has(piece.amount)) {
1028
+ add([...pieceBase, "amount"], `piece amount ${piece.amount} cannot be updated`, "UDL4002");
1029
+ }
1030
+ const releaseSchema = instrument.fields[piece.release_to];
1031
+ if (!releaseSchema || !references.accepts(releaseSchema, "acct")) {
1032
+ add([...pieceBase, "release_to"], `piece release_to ${piece.release_to} must be a declared account field`, "UDL4002");
1033
+ }
1034
+ if (!instrument.required.includes(piece.release_to)) {
1035
+ add([...pieceBase, "release_to"], `piece release_to ${piece.release_to} must be required`, "UDL4002");
1036
+ }
1037
+ if (mutableFields.has(piece.release_to) ||
1038
+ allUpdatedFields.has(piece.release_to)) {
1039
+ add([...pieceBase, "release_to"], `piece release_to ${piece.release_to} cannot be updated`, "UDL4002");
1040
+ }
1041
+ const refundSchema = instrument.fields[piece.refund_to];
1042
+ if (!refundSchema || !references.accepts(refundSchema, "acct")) {
1043
+ add([...pieceBase, "refund_to"], `piece refund_to ${piece.refund_to} must be a declared account field`, "UDL4002");
1044
+ }
1045
+ if (!instrument.required.includes(piece.refund_to)) {
1046
+ add([...pieceBase, "refund_to"], `piece refund_to ${piece.refund_to} must be required`, "UDL4002");
1047
+ }
1048
+ if (mutableFields.has(piece.refund_to) ||
1049
+ allUpdatedFields.has(piece.refund_to)) {
1050
+ add([...pieceBase, "refund_to"], `piece refund_to ${piece.refund_to} cannot be updated`, "UDL4002");
1051
+ }
1052
+ });
1053
+ // Check currency agreement
1054
+ const totalCurrency = totalSchema && typeof totalSchema["x-hyperscale-currency"] === "string"
1055
+ ? totalSchema["x-hyperscale-currency"]
1056
+ : undefined;
1057
+ const pieceCurrencies = plan.pieces.map((p) => {
1058
+ const s = instrument.fields[p.amount];
1059
+ return s && typeof s["x-hyperscale-currency"] === "string"
1060
+ ? s["x-hyperscale-currency"]
1061
+ : undefined;
1062
+ });
1063
+ const allCurrencies = [totalCurrency, ...pieceCurrencies];
1064
+ const definedCurrencies = allCurrencies.filter((c) => c !== undefined);
1065
+ if (definedCurrencies.length > 0) {
1066
+ const firstCurrency = definedCurrencies[0];
1067
+ if (definedCurrencies.length !== allCurrencies.length ||
1068
+ definedCurrencies.some((c) => c !== firstCurrency)) {
1069
+ add(planBase, `piece plan money fields must agree on one declared currency`, "UDL4002");
1070
+ }
1071
+ else if (!/^[A-Z]{3}$/.test(firstCurrency)) {
1072
+ add(planBase, `piece plan currency ${firstCurrency} must be a concrete ISO currency`, "UDL4002");
1073
+ }
1074
+ }
1075
+ else {
1076
+ const concreteCurrency = (() => {
1077
+ const currencyFields = Object.entries(instrument.fields).filter(([, schema]) => isCurrencySchema(schema));
1078
+ if (currencyFields.length === 1) {
1079
+ const [fName, fSchema] = currencyFields[0];
1080
+ if (mutableFields.has(fName) || allUpdatedFields.has(fName)) {
1081
+ return undefined;
1082
+ }
1083
+ if (typeof fSchema.const === "string" &&
1084
+ /^[A-Z]{3}$/.test(fSchema.const)) {
1085
+ return fSchema.const;
1086
+ }
1087
+ if (Array.isArray(fSchema.enum) &&
1088
+ fSchema.enum.length === 1 &&
1089
+ typeof fSchema.enum[0] === "string" &&
1090
+ /^[A-Z]{3}$/.test(fSchema.enum[0])) {
1091
+ return fSchema.enum[0];
1092
+ }
1093
+ }
1094
+ return undefined;
1095
+ })();
1096
+ if (!concreteCurrency) {
1097
+ add(planBase, `piece plan requires an actual single concrete currency via x-hyperscale-currency tags or an immutable concrete instrument currency declaration`, "UDL4002");
1098
+ }
1099
+ }
1100
+ // Disallow updates of the instance currency field
1101
+ const instanceCurrencyFields = Object.entries(instrument.fields)
1102
+ .filter(([, schema]) => isCurrencySchema(schema))
1103
+ .map(([field]) => field);
1104
+ for (const cField of instanceCurrencyFields) {
1105
+ if (mutableFields.has(cField) || allUpdatedFields.has(cField)) {
1106
+ add([...planBase, "currency"], `piece plan instance currency field ${cField} cannot be updated`, "UDL4002");
1107
+ }
1108
+ }
1109
+ // Check fixed amounts against total
1110
+ const totalVal = getFixedMoneyValue(totalSchema);
1111
+ const pieceVals = plan.pieces.map((p) => getFixedMoneyValue(instrument.fields[p.amount]));
1112
+ if (totalVal !== undefined &&
1113
+ pieceVals.every((v) => v !== undefined)) {
1114
+ const sum = pieceVals.reduce((acc, v) => acc + v, 0n);
1115
+ if (sum !== totalVal) {
1116
+ add([...planBase, "total"], `sum of fixed piece amounts (${sum}) does not equal plan total (${totalVal})`, "UDL4002");
1117
+ }
1118
+ }
1119
+ // Check partition
1120
+ const isSingletonTotal = plan.pieces.length === 1 && plan.pieces[0].amount === plan.total;
1121
+ if (new Set(pieceAmountFields).size !== pieceAmountFields.length) {
1122
+ add([...planBase, "pieces"], `piece amount fields must be distinct: [${pieceAmountFields.join(", ")}]`, "UDL4002");
1123
+ }
1124
+ else if (!isSingletonTotal) {
1125
+ const planAmounts = [...pieceAmountFields].sort();
1126
+ const matchingPartition = (instrument.partitions ?? []).find((p) => {
1127
+ if (p.totalField !== plan.total)
1128
+ return false;
1129
+ const partAmounts = [...p.pieceFields].sort();
1130
+ return (partAmounts.length === planAmounts.length &&
1131
+ partAmounts.every((f, i) => f === planAmounts[i]));
1132
+ });
1133
+ if (!matchingPartition) {
1134
+ add(planBase, `piece plan must match a declared partition with totalField ${plan.total} and pieceFields [${pieceAmountFields.join(", ")}]`, "UDL4002");
1135
+ }
1136
+ }
1137
+ // Check orders (UDL5013)
1138
+ const validateOrder = (order, orderName, exactSet) => {
1139
+ const orderPath = [...planBase, orderName];
1140
+ if (new Set(order).size !== order.length) {
1141
+ add(orderPath, `${orderName} contains duplicate piece ids`, "UDL5013");
1142
+ }
1143
+ for (const id of order) {
1144
+ if (!pieceIds.has(id)) {
1145
+ add(orderPath, `${orderName} contains undeclared piece id ${id}`, "UDL5013");
1146
+ }
1147
+ }
1148
+ if (exactSet) {
1149
+ if (order.length !== pieceIds.size) {
1150
+ add(orderPath, `${orderName} must cover every declared piece id`, "UDL5013");
1151
+ }
1152
+ }
1153
+ };
1154
+ validateOrder(plan.fund_order, "fund_order", true);
1155
+ validateOrder(plan.release_order, "release_order", false);
1156
+ validateOrder(plan.refund_order, "refund_order", false);
1157
+ validateOrder(plan.unfund_order, "unfund_order", true);
1158
+ if (plan.fund_order.length === plan.unfund_order.length &&
1159
+ !plan.unfund_order.every((id, idx) => id === plan.fund_order[plan.fund_order.length - 1 - idx])) {
1160
+ add([...planBase, "unfund_order"], `unfund_order must be the reverse of fund_order`, "UDL5013");
1161
+ }
1162
+ }
1163
+ function getFixedMoneyValue(schema) {
1164
+ if (!schema || typeof schema !== "object")
1165
+ return undefined;
1166
+ const s = schema;
1167
+ if (typeof s.const === "string" && /^[0-9]+$/.test(s.const)) {
1168
+ return BigInt(s.const);
1169
+ }
1170
+ if (Array.isArray(s.enum) &&
1171
+ s.enum.length === 1 &&
1172
+ typeof s.enum[0] === "string" &&
1173
+ /^[0-9]+$/.test(s.enum[0])) {
1174
+ return BigInt(s.enum[0]);
1175
+ }
1176
+ return undefined;
1177
+ }
893
1178
  function validateFeeRules(instrument, base, references, add) {
894
1179
  const feeRules = instrument.feeRules ?? [];
895
1180
  if (feeRules.length === 0)