@hyperscale0/udl 2.0.4 → 2.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.
Files changed (58) hide show
  1. package/CHANGELOG.md +23 -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 +26 -3
  22. package/dist/effects.d.ts.map +1 -1
  23. package/dist/effects.js +962 -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 +1672 -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 +470 -5
package/src/validation.ts CHANGED
@@ -26,7 +26,13 @@ import {
26
26
  financeAdmissionProblem,
27
27
  } from "./finance.js";
28
28
  import { UDL_LIMITS } from "./limits.js";
29
- import { deriveUdlActionEffects, udlEffectKinds } from "./effects.js";
29
+ import {
30
+ deriveUdlActionEffects,
31
+ resolveUdlActionPlans,
32
+ udlEffectKinds,
33
+ type DerivedUdlEffects,
34
+ type ResolvedActionPlan,
35
+ } from "./effects.js";
30
36
  import { issue, type UdlIssue, type UdlIssueCode } from "./diagnostics.js";
31
37
 
32
38
  export type {
@@ -329,6 +335,7 @@ function semanticIssues(
329
335
  }
330
336
 
331
337
  for (const [instrumentIndex, instrument] of document.instruments.entries()) {
338
+ const resolvedPlansResult = resolveUdlActionPlans(instrument);
332
339
  for (const [actionName, action] of Object.entries(instrument.actions)) {
333
340
  if (options.requireDecisionPartyBindings) {
334
341
  for (const [index, role] of (
@@ -352,7 +359,15 @@ function semanticIssues(
352
359
  }
353
360
  }
354
361
  if (!action.effects) continue;
355
- const expected = deriveUdlActionEffects(action, udlClauseVocabulary);
362
+ let expected: DerivedUdlEffects;
363
+ if (action.calls && action.calls.length > 0) {
364
+ const plan = resolvedPlansResult.plans.find(
365
+ (p) => p.action === actionName,
366
+ );
367
+ expected = plan ? plan.effects : {};
368
+ } else {
369
+ expected = deriveUdlActionEffects(action, udlClauseVocabulary);
370
+ }
356
371
  for (const kind of udlEffectKinds) {
357
372
  const actualRows = action.effects[kind] ?? [];
358
373
  const expectedRows = expected[kind] ?? [];
@@ -1320,12 +1335,462 @@ function validateInstrument(
1320
1335
  validateSetsAt(instrument, base, add);
1321
1336
  validateActions(instrument, base, instruments, references, add);
1322
1337
  validateQuoteCommit(instrument, base, references, add);
1323
- for (const issue of analyzeInstrumentFinance(instrument)) {
1324
- add([...base, ...issue.path], issue.message, issue.code);
1338
+ validatePiecePlan(instrument, base, references, add);
1339
+
1340
+ const planResolution = resolveUdlActionPlans(instrument);
1341
+ for (const planIssue of planResolution.issues) {
1342
+ const rawPath = planIssue.path.startsWith("$.")
1343
+ ? planIssue.path.slice(2).split(".")
1344
+ : [planIssue.path];
1345
+ add([...base, ...rawPath], planIssue.message, planIssue.code);
1346
+ }
1347
+
1348
+ // Validate leaf steps
1349
+ const validatedLeaves = new Set<string>();
1350
+ for (const plan of planResolution.plans) {
1351
+ const actionDef = instrument.actions[plan.action];
1352
+ if (!actionDef) continue;
1353
+ for (const leaf of plan.leaves) {
1354
+ const leafKey = `${plan.pieceId ?? ""}:${leaf.originPath.join(".")}`;
1355
+ if (!validatedLeaves.has(leafKey)) {
1356
+ validatedLeaves.add(leafKey);
1357
+ validateStep(
1358
+ instrument,
1359
+ actionDef,
1360
+ leaf.step,
1361
+ [...base, "actions", ...leaf.originPath],
1362
+ add,
1363
+ );
1364
+ }
1365
+ }
1366
+ }
1367
+
1368
+ // Multi-variant finance oracle check across action plan combinations
1369
+ const actionsWithPlans = Object.keys(instrument.actions).filter((aName) =>
1370
+ planResolution.plans.some((p) => p.action === aName),
1371
+ );
1372
+
1373
+ let totalCombinations = 1;
1374
+ const actionPlanMap: Record<string, ResolvedActionPlan[]> = {};
1375
+ for (const aName of actionsWithPlans) {
1376
+ const actionPlans = planResolution.plans.filter((p) => p.action === aName);
1377
+ actionPlanMap[aName] = actionPlans;
1378
+ totalCombinations *= actionPlans.length;
1379
+ }
1380
+
1381
+ if (
1382
+ actionsWithPlans.length > 0 &&
1383
+ totalCombinations > UDL_LIMITS.maxActionExpansion
1384
+ ) {
1385
+ add(
1386
+ [...base, "actions"],
1387
+ `variant expansion exceeds combination bound of ${UDL_LIMITS.maxActionExpansion} (${totalCombinations} combinations)`,
1388
+ "UDL2010",
1389
+ );
1390
+ } else {
1391
+ const generateCombos = (
1392
+ keys: string[],
1393
+ ): Record<string, ResolvedActionPlan>[] => {
1394
+ if (keys.length === 0) return [{}];
1395
+ const [first, ...rest] = keys;
1396
+ const restCombos = generateCombos(rest);
1397
+ const result: Record<string, ResolvedActionPlan>[] = [];
1398
+ for (const plan of actionPlanMap[first!]!) {
1399
+ for (const combo of restCombos) {
1400
+ result.push({ ...combo, [first!]: plan });
1401
+ }
1402
+ }
1403
+ return result;
1404
+ };
1405
+
1406
+ const combinations =
1407
+ actionsWithPlans.length > 0 ? generateCombos(actionsWithPlans) : [{}];
1408
+ const seenFinanceIssues = new Set<string>();
1409
+
1410
+ for (const combo of combinations) {
1411
+ const expandedActions: Record<
1412
+ string,
1413
+ (typeof instrument.actions)[string]
1414
+ > = {};
1415
+ for (const [aName, aDef] of Object.entries(instrument.actions)) {
1416
+ const plan = combo[aName];
1417
+ // Only an action that moves money through calls is replaced by its
1418
+ // expanded leaves. Authored moves and steps always reach the oracle.
1419
+ if (plan && (aDef.calls?.length ?? 0) > 0) {
1420
+ const steps: UdlStep[] = [];
1421
+ const moves: UdlMove[] = [];
1422
+ for (const leaf of plan.leaves) {
1423
+ if ("key" in leaf.step) {
1424
+ moves.push(leaf.step as UdlMove);
1425
+ } else {
1426
+ steps.push(leaf.step as UdlStep);
1427
+ }
1428
+ }
1429
+ expandedActions[aName] = {
1430
+ ...aDef,
1431
+ moves,
1432
+ steps,
1433
+ };
1434
+ } else {
1435
+ expandedActions[aName] = aDef;
1436
+ }
1437
+ }
1438
+ const financeInstrument = {
1439
+ ...instrument,
1440
+ actions: expandedActions,
1441
+ };
1442
+ for (const finIssue of analyzeInstrumentFinance(financeInstrument)) {
1443
+ const issueKey = `${finIssue.code}:${finIssue.path.join(".")}:${finIssue.message}`;
1444
+ if (!seenFinanceIssues.has(issueKey)) {
1445
+ seenFinanceIssues.add(issueKey);
1446
+ add([...base, ...finIssue.path], finIssue.message, finIssue.code);
1447
+ }
1448
+ }
1449
+ }
1325
1450
  }
1326
1451
  validateAggregates(instrument, base, instruments, references, add);
1327
1452
  }
1328
1453
 
1454
+ function validatePiecePlan(
1455
+ instrument: UdlInstrument,
1456
+ base: readonly PropertyKey[],
1457
+ references: ReferenceShapeBudget,
1458
+ add: AddIssue,
1459
+ ): void {
1460
+ const plan = instrument.piecePlan;
1461
+ if (!plan) return;
1462
+
1463
+ const planBase = [...base, "piecePlan"] as const;
1464
+ const mutableFields = new Set(instrument.update?.fields ?? []);
1465
+ const allUpdatedFields = new Set(
1466
+ Object.values(instrument.actions).flatMap((a) => a.updates ?? []),
1467
+ );
1468
+
1469
+ // Validate total field
1470
+ const totalSchema = instrument.fields[plan.total];
1471
+ if (
1472
+ !totalSchema ||
1473
+ totalSchema.type !== "string" ||
1474
+ !isMoneySchema(totalSchema)
1475
+ ) {
1476
+ add(
1477
+ [...planBase, "total"],
1478
+ `piece plan total ${plan.total} must be a declared money field`,
1479
+ "UDL4002",
1480
+ );
1481
+ }
1482
+ if (!instrument.required.includes(plan.total)) {
1483
+ add(
1484
+ [...planBase, "total"],
1485
+ `piece plan total ${plan.total} must be required`,
1486
+ "UDL4002",
1487
+ );
1488
+ }
1489
+ if (mutableFields.has(plan.total) || allUpdatedFields.has(plan.total)) {
1490
+ add(
1491
+ [...planBase, "total"],
1492
+ `piece plan total ${plan.total} cannot be updated`,
1493
+ "UDL4002",
1494
+ );
1495
+ }
1496
+
1497
+ const pieceIds = new Set<string>();
1498
+ const pieceAmountFields: string[] = [];
1499
+
1500
+ plan.pieces.forEach((piece, index) => {
1501
+ const pieceBase = [...planBase, "pieces", index] as const;
1502
+ if (pieceIds.has(piece.id)) {
1503
+ add(
1504
+ [...pieceBase, "id"],
1505
+ `duplicate piece id ${piece.id} in piece plan`,
1506
+ "UDL4002",
1507
+ );
1508
+ }
1509
+ pieceIds.add(piece.id);
1510
+ pieceAmountFields.push(piece.amount);
1511
+
1512
+ const amountSchema = instrument.fields[piece.amount];
1513
+ if (
1514
+ !amountSchema ||
1515
+ amountSchema.type !== "string" ||
1516
+ !isMoneySchema(amountSchema)
1517
+ ) {
1518
+ add(
1519
+ [...pieceBase, "amount"],
1520
+ `piece amount ${piece.amount} must be a declared money field`,
1521
+ "UDL4002",
1522
+ );
1523
+ }
1524
+ if (!instrument.required.includes(piece.amount)) {
1525
+ add(
1526
+ [...pieceBase, "amount"],
1527
+ `piece amount ${piece.amount} must be required`,
1528
+ "UDL4002",
1529
+ );
1530
+ }
1531
+ if (mutableFields.has(piece.amount) || allUpdatedFields.has(piece.amount)) {
1532
+ add(
1533
+ [...pieceBase, "amount"],
1534
+ `piece amount ${piece.amount} cannot be updated`,
1535
+ "UDL4002",
1536
+ );
1537
+ }
1538
+
1539
+ const releaseSchema = instrument.fields[piece.release_to];
1540
+ if (!releaseSchema || !references.accepts(releaseSchema, "acct")) {
1541
+ add(
1542
+ [...pieceBase, "release_to"],
1543
+ `piece release_to ${piece.release_to} must be a declared account field`,
1544
+ "UDL4002",
1545
+ );
1546
+ }
1547
+ if (!instrument.required.includes(piece.release_to)) {
1548
+ add(
1549
+ [...pieceBase, "release_to"],
1550
+ `piece release_to ${piece.release_to} must be required`,
1551
+ "UDL4002",
1552
+ );
1553
+ }
1554
+ if (
1555
+ mutableFields.has(piece.release_to) ||
1556
+ allUpdatedFields.has(piece.release_to)
1557
+ ) {
1558
+ add(
1559
+ [...pieceBase, "release_to"],
1560
+ `piece release_to ${piece.release_to} cannot be updated`,
1561
+ "UDL4002",
1562
+ );
1563
+ }
1564
+
1565
+ const refundSchema = instrument.fields[piece.refund_to];
1566
+ if (!refundSchema || !references.accepts(refundSchema, "acct")) {
1567
+ add(
1568
+ [...pieceBase, "refund_to"],
1569
+ `piece refund_to ${piece.refund_to} must be a declared account field`,
1570
+ "UDL4002",
1571
+ );
1572
+ }
1573
+ if (!instrument.required.includes(piece.refund_to)) {
1574
+ add(
1575
+ [...pieceBase, "refund_to"],
1576
+ `piece refund_to ${piece.refund_to} must be required`,
1577
+ "UDL4002",
1578
+ );
1579
+ }
1580
+ if (
1581
+ mutableFields.has(piece.refund_to) ||
1582
+ allUpdatedFields.has(piece.refund_to)
1583
+ ) {
1584
+ add(
1585
+ [...pieceBase, "refund_to"],
1586
+ `piece refund_to ${piece.refund_to} cannot be updated`,
1587
+ "UDL4002",
1588
+ );
1589
+ }
1590
+ });
1591
+
1592
+ // Check currency agreement
1593
+ const totalCurrency =
1594
+ totalSchema && typeof totalSchema["x-hyperscale-currency"] === "string"
1595
+ ? totalSchema["x-hyperscale-currency"]
1596
+ : undefined;
1597
+
1598
+ const pieceCurrencies = plan.pieces.map((p) => {
1599
+ const s = instrument.fields[p.amount];
1600
+ return s && typeof s["x-hyperscale-currency"] === "string"
1601
+ ? s["x-hyperscale-currency"]
1602
+ : undefined;
1603
+ });
1604
+
1605
+ const allCurrencies = [totalCurrency, ...pieceCurrencies];
1606
+ const definedCurrencies = allCurrencies.filter(
1607
+ (c): c is string => c !== undefined,
1608
+ );
1609
+
1610
+ if (definedCurrencies.length > 0) {
1611
+ const firstCurrency = definedCurrencies[0]!;
1612
+ if (
1613
+ definedCurrencies.length !== allCurrencies.length ||
1614
+ definedCurrencies.some((c) => c !== firstCurrency)
1615
+ ) {
1616
+ add(
1617
+ planBase,
1618
+ `piece plan money fields must agree on one declared currency`,
1619
+ "UDL4002",
1620
+ );
1621
+ } else if (!/^[A-Z]{3}$/.test(firstCurrency)) {
1622
+ add(
1623
+ planBase,
1624
+ `piece plan currency ${firstCurrency} must be a concrete ISO currency`,
1625
+ "UDL4002",
1626
+ );
1627
+ }
1628
+ } else {
1629
+ const concreteCurrency = ((): string | undefined => {
1630
+ const currencyFields = Object.entries(instrument.fields).filter(
1631
+ ([, schema]) => isCurrencySchema(schema),
1632
+ );
1633
+ if (currencyFields.length === 1) {
1634
+ const [fName, fSchema] = currencyFields[0]!;
1635
+ if (mutableFields.has(fName) || allUpdatedFields.has(fName)) {
1636
+ return undefined;
1637
+ }
1638
+ if (
1639
+ typeof fSchema.const === "string" &&
1640
+ /^[A-Z]{3}$/.test(fSchema.const)
1641
+ ) {
1642
+ return fSchema.const;
1643
+ }
1644
+ if (
1645
+ Array.isArray(fSchema.enum) &&
1646
+ fSchema.enum.length === 1 &&
1647
+ typeof fSchema.enum[0] === "string" &&
1648
+ /^[A-Z]{3}$/.test(fSchema.enum[0])
1649
+ ) {
1650
+ return fSchema.enum[0];
1651
+ }
1652
+ }
1653
+ return undefined;
1654
+ })();
1655
+
1656
+ if (!concreteCurrency) {
1657
+ add(
1658
+ planBase,
1659
+ `piece plan requires an actual single concrete currency via x-hyperscale-currency tags or an immutable concrete instrument currency declaration`,
1660
+ "UDL4002",
1661
+ );
1662
+ }
1663
+ }
1664
+
1665
+ // Disallow updates of the instance currency field
1666
+ const instanceCurrencyFields = Object.entries(instrument.fields)
1667
+ .filter(([, schema]) => isCurrencySchema(schema))
1668
+ .map(([field]) => field);
1669
+ for (const cField of instanceCurrencyFields) {
1670
+ if (mutableFields.has(cField) || allUpdatedFields.has(cField)) {
1671
+ add(
1672
+ [...planBase, "currency"],
1673
+ `piece plan instance currency field ${cField} cannot be updated`,
1674
+ "UDL4002",
1675
+ );
1676
+ }
1677
+ }
1678
+
1679
+ // Check fixed amounts against total
1680
+ const totalVal = getFixedMoneyValue(totalSchema);
1681
+ const pieceVals = plan.pieces.map((p) =>
1682
+ getFixedMoneyValue(instrument.fields[p.amount]),
1683
+ );
1684
+ if (
1685
+ totalVal !== undefined &&
1686
+ pieceVals.every((v): v is bigint => v !== undefined)
1687
+ ) {
1688
+ const sum = pieceVals.reduce((acc, v) => acc + v, 0n);
1689
+ if (sum !== totalVal) {
1690
+ add(
1691
+ [...planBase, "total"],
1692
+ `sum of fixed piece amounts (${sum}) does not equal plan total (${totalVal})`,
1693
+ "UDL4002",
1694
+ );
1695
+ }
1696
+ }
1697
+
1698
+ // Check partition
1699
+ const isSingletonTotal =
1700
+ plan.pieces.length === 1 && plan.pieces[0]!.amount === plan.total;
1701
+
1702
+ if (new Set(pieceAmountFields).size !== pieceAmountFields.length) {
1703
+ add(
1704
+ [...planBase, "pieces"],
1705
+ `piece amount fields must be distinct: [${pieceAmountFields.join(", ")}]`,
1706
+ "UDL4002",
1707
+ );
1708
+ } else if (!isSingletonTotal) {
1709
+ const planAmounts = [...pieceAmountFields].sort();
1710
+ const matchingPartition = (instrument.partitions ?? []).find((p) => {
1711
+ if (p.totalField !== plan.total) return false;
1712
+ const partAmounts = [...p.pieceFields].sort();
1713
+ return (
1714
+ partAmounts.length === planAmounts.length &&
1715
+ partAmounts.every((f, i) => f === planAmounts[i])
1716
+ );
1717
+ });
1718
+
1719
+ if (!matchingPartition) {
1720
+ add(
1721
+ planBase,
1722
+ `piece plan must match a declared partition with totalField ${plan.total} and pieceFields [${pieceAmountFields.join(", ")}]`,
1723
+ "UDL4002",
1724
+ );
1725
+ }
1726
+ }
1727
+
1728
+ // Check orders (UDL5013)
1729
+ const validateOrder = (
1730
+ order: readonly string[],
1731
+ orderName: string,
1732
+ exactSet: boolean,
1733
+ ): void => {
1734
+ const orderPath = [...planBase, orderName] as const;
1735
+ if (new Set(order).size !== order.length) {
1736
+ add(orderPath, `${orderName} contains duplicate piece ids`, "UDL5013");
1737
+ }
1738
+ for (const id of order) {
1739
+ if (!pieceIds.has(id)) {
1740
+ add(
1741
+ orderPath,
1742
+ `${orderName} contains undeclared piece id ${id}`,
1743
+ "UDL5013",
1744
+ );
1745
+ }
1746
+ }
1747
+ if (exactSet) {
1748
+ if (order.length !== pieceIds.size) {
1749
+ add(
1750
+ orderPath,
1751
+ `${orderName} must cover every declared piece id`,
1752
+ "UDL5013",
1753
+ );
1754
+ }
1755
+ }
1756
+ };
1757
+
1758
+ validateOrder(plan.fund_order, "fund_order", true);
1759
+ validateOrder(plan.release_order, "release_order", false);
1760
+ validateOrder(plan.refund_order, "refund_order", false);
1761
+ validateOrder(plan.unfund_order, "unfund_order", true);
1762
+
1763
+ if (
1764
+ plan.fund_order.length === plan.unfund_order.length &&
1765
+ !plan.unfund_order.every(
1766
+ (id, idx) => id === plan.fund_order[plan.fund_order.length - 1 - idx],
1767
+ )
1768
+ ) {
1769
+ add(
1770
+ [...planBase, "unfund_order"],
1771
+ `unfund_order must be the reverse of fund_order`,
1772
+ "UDL5013",
1773
+ );
1774
+ }
1775
+ }
1776
+
1777
+ function getFixedMoneyValue(schema: unknown): bigint | undefined {
1778
+ if (!schema || typeof schema !== "object") return undefined;
1779
+ const s = schema as Record<string, unknown>;
1780
+ if (typeof s.const === "string" && /^[0-9]+$/.test(s.const)) {
1781
+ return BigInt(s.const);
1782
+ }
1783
+ if (
1784
+ Array.isArray(s.enum) &&
1785
+ s.enum.length === 1 &&
1786
+ typeof s.enum[0] === "string" &&
1787
+ /^[0-9]+$/.test(s.enum[0])
1788
+ ) {
1789
+ return BigInt(s.enum[0]);
1790
+ }
1791
+ return undefined;
1792
+ }
1793
+
1329
1794
  function validateFeeRules(
1330
1795
  instrument: UdlInstrument,
1331
1796
  base: readonly PropertyKey[],
@@ -3990,7 +4455,7 @@ function referencedPathDeclared(target: UdlInstrument, path: string): boolean {
3990
4455
  function validateStep(
3991
4456
  instrument: UdlInstrument,
3992
4457
  action: UdlAction,
3993
- step: UdlStep | UdlMove,
4458
+ step: ResolvedActionPlan["leaves"][number]["step"],
3994
4459
  base: readonly PropertyKey[],
3995
4460
  add: AddIssue,
3996
4461
  ): void {