@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.
- package/CHANGELOG.md +16 -0
- package/README.md +15 -1
- package/conformance/invalid/call-binds-results.expected.json +10 -0
- package/conformance/invalid/call-binds-results.udl +314 -0
- package/conformance/invalid/call-unknown-action.expected.json +10 -0
- package/conformance/invalid/call-unknown-action.udl +314 -0
- package/conformance/invalid/leaf-effect-mismatch.expected.json +10 -0
- package/conformance/invalid/leaf-effect-mismatch.udl +314 -0
- package/conformance/invalid/piece-plan-without-partition.expected.json +10 -0
- package/conformance/invalid/piece-plan-without-partition.udl +305 -0
- package/conformance/invalid/private-action-independent-approval.expected.json +10 -0
- package/conformance/invalid/private-action-independent-approval.udl +314 -0
- package/conformance/invalid/unfund-order-not-reversed.expected.json +10 -0
- package/conformance/invalid/unfund-order-not-reversed.udl +314 -0
- package/conformance/valid/piece-plan-calls.expected.json +6 -0
- package/conformance/valid/piece-plan-calls.udl +314 -0
- package/dist/diagnostics.d.ts +36 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +36 -0
- package/dist/diagnostics.js.map +1 -1
- package/dist/effects.d.ts +24 -3
- package/dist/effects.d.ts.map +1 -1
- package/dist/effects.js +906 -8
- package/dist/effects.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +2 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +2 -0
- package/dist/limits.js.map +1 -1
- package/dist/schema.d.ts +594 -11
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +111 -1
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +289 -4
- package/dist/validation.js.map +1 -1
- package/docs/assets/brand/manifest.json +30 -0
- package/docs/assets/brand/udl-horizontal-white.svg +1 -0
- package/docs/assets/brand/udl-horizontal.svg +1 -0
- package/docs/assets/brand/udl-stacked-white.svg +1 -0
- package/docs/assets/brand/udl-stacked.svg +1 -0
- package/docs/assets/udl.svg +7 -14
- package/docs/llms-full.txt +199 -32
- package/docs/llms.txt +1 -1
- package/docs/reference/clauses.md +162 -1
- package/docs/reference/cli.md +1 -1
- package/docs/reference/diagnostics.md +38 -32
- package/package.json +2 -2
- package/spec/udl.schema.json +321 -1
- package/src/diagnostics.ts +36 -0
- package/src/effects.ts +1603 -9
- package/src/index.ts +15 -0
- package/src/limits.ts +2 -0
- package/src/schema.ts +149 -2
- package/src/validation.ts +469 -4
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 {
|
|
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
|
-
|
|
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
|
-
|
|
1324
|
-
|
|
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[],
|