@hyperscale0/udl 2.2.0 → 2.4.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 +38 -0
- package/README.md +5 -1
- package/conformance/invalid/invalid-journeys.expected.json +3 -19
- package/conformance/invalid/invalid-journeys.udl +47 -49
- package/conformance/valid/attested.expected.json +6 -0
- package/conformance/valid/attested.udl +251 -0
- package/conformance/valid/hand-edited.expected.json +1 -1
- package/conformance/valid/hand-edited.udl +1 -1
- package/conformance/valid/minimal.expected.json +1 -1
- package/conformance/valid/minimal.udl +0 -15
- package/conformance/valid/vocabulary.expected.json +6 -0
- package/conformance/valid/vocabulary.udl +1999 -0
- package/dist/allocation.d.ts +60 -0
- package/dist/allocation.d.ts.map +1 -0
- package/dist/allocation.js +177 -0
- package/dist/allocation.js.map +1 -0
- package/dist/diagnostics.d.ts +1 -31
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +0 -30
- package/dist/diagnostics.js.map +1 -1
- package/dist/distribution.d.ts +15 -0
- package/dist/distribution.d.ts.map +1 -0
- package/dist/distribution.js +49 -0
- package/dist/distribution.js.map +1 -0
- package/dist/effects.d.ts +0 -6
- package/dist/effects.d.ts.map +1 -1
- package/dist/effects.js +84 -22
- package/dist/effects.js.map +1 -1
- package/dist/evolution.d.ts +12 -0
- package/dist/evolution.d.ts.map +1 -1
- package/dist/evolution.js +42 -1
- package/dist/evolution.js.map +1 -1
- package/dist/finance.d.ts +18 -1
- package/dist/finance.d.ts.map +1 -1
- package/dist/finance.js +158 -27
- package/dist/finance.js.map +1 -1
- package/dist/index.d.ts +9 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/instrument-references.d.ts +5 -0
- package/dist/instrument-references.d.ts.map +1 -0
- package/dist/instrument-references.js +69 -0
- package/dist/instrument-references.js.map +1 -0
- package/dist/limits.d.ts +3 -3
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +3 -7
- package/dist/limits.js.map +1 -1
- package/dist/reference.d.ts +3 -0
- package/dist/reference.d.ts.map +1 -0
- package/dist/reference.js +28 -0
- package/dist/reference.js.map +1 -0
- package/dist/schema.d.ts +1232 -83
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +398 -60
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts +27 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +327 -187
- package/dist/validation.js.map +1 -1
- package/dist/vocabulary.d.ts +23 -0
- package/dist/vocabulary.d.ts.map +1 -0
- package/dist/vocabulary.js +965 -0
- package/dist/vocabulary.js.map +1 -0
- package/docs/README.md +5 -1
- package/docs/funding-custody.md +165 -0
- package/docs/guide/09-schedules-and-allocation.md +130 -0
- package/docs/llms-full.txt +623 -101
- package/docs/llms.txt +1 -1
- package/docs/piece-plans.md +148 -0
- package/docs/reference/clauses.md +451 -63
- package/docs/reference/cli.md +3 -1
- package/docs/reference/diagnostics.md +33 -38
- package/package.json +5 -6
- package/spec/udl.schema.json +1095 -118
- package/src/allocation.ts +259 -0
- package/src/diagnostics.ts +0 -32
- package/src/distribution.ts +61 -0
- package/src/effects.ts +116 -22
- package/src/evolution.ts +63 -3
- package/src/finance.ts +218 -24
- package/src/index.ts +31 -3
- package/src/instrument-references.ts +98 -0
- package/src/limits.ts +3 -7
- package/src/reference.ts +31 -0
- package/src/schema.ts +417 -66
- package/src/validation.ts +452 -243
- package/src/vocabulary.ts +1508 -0
package/src/finance.ts
CHANGED
|
@@ -22,7 +22,24 @@ interface FinancialMove extends FinancialStep {
|
|
|
22
22
|
readonly key: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
interface
|
|
25
|
+
export interface FinancialPartition {
|
|
26
|
+
readonly totalField: string;
|
|
27
|
+
readonly pieceFields: readonly string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FinancialContribution {
|
|
31
|
+
readonly field: string;
|
|
32
|
+
readonly amountKey: string;
|
|
33
|
+
readonly accountKey: string;
|
|
34
|
+
readonly totalField: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface FinancialContributionStage {
|
|
38
|
+
readonly stage: "fund" | "refund";
|
|
39
|
+
readonly accountPath: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FinancialInstrument {
|
|
26
43
|
readonly lifecycle: {
|
|
27
44
|
readonly initial: string;
|
|
28
45
|
readonly states: readonly string[];
|
|
@@ -37,6 +54,8 @@ interface FinancialInstrument {
|
|
|
37
54
|
readonly subjectHolder?: string | undefined;
|
|
38
55
|
}
|
|
39
56
|
| undefined;
|
|
57
|
+
readonly partitions?: readonly FinancialPartition[] | undefined;
|
|
58
|
+
readonly contributions?: FinancialContribution | undefined;
|
|
40
59
|
readonly actions: Readonly<
|
|
41
60
|
Record<
|
|
42
61
|
string,
|
|
@@ -57,6 +76,7 @@ interface FinancialInstrument {
|
|
|
57
76
|
}
|
|
58
77
|
| undefined;
|
|
59
78
|
readonly steps: readonly FinancialStep[];
|
|
79
|
+
readonly contributionStage?: FinancialContributionStage | undefined;
|
|
60
80
|
}
|
|
61
81
|
>
|
|
62
82
|
>;
|
|
@@ -90,6 +110,7 @@ type Balance =
|
|
|
90
110
|
interface AccountState {
|
|
91
111
|
readonly balance: Balance;
|
|
92
112
|
readonly holds: Readonly<Record<string, Balance>>;
|
|
113
|
+
readonly exchanged?: boolean | undefined;
|
|
93
114
|
}
|
|
94
115
|
|
|
95
116
|
interface Reservation {
|
|
@@ -348,13 +369,9 @@ function productEscrowAccounts(instrument: FinancialInstrument): Set<string> {
|
|
|
348
369
|
const accounts = new Set<string>();
|
|
349
370
|
for (const action of Object.values(instrument.actions)) {
|
|
350
371
|
for (const step of action.steps) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
step.bind.role.value !== "product_escrow"
|
|
355
|
-
) {
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
372
|
+
// This operation fixes the escrow role; an omitted redundant binding
|
|
373
|
+
// must not exempt its captured account from the conservation proof.
|
|
374
|
+
if (step.operation !== "account.escrow.provision") continue;
|
|
358
375
|
for (const [key, result] of Object.entries(step.capture ?? {})) {
|
|
359
376
|
if (result === "accountId") accounts.add(`ref:${key}`);
|
|
360
377
|
}
|
|
@@ -366,8 +383,17 @@ function productEscrowAccounts(instrument: FinancialInstrument): Set<string> {
|
|
|
366
383
|
// remain runtime balance checks. Commit sources are added separately above.
|
|
367
384
|
return new Set(
|
|
368
385
|
[...accounts].filter((account) =>
|
|
369
|
-
Object.values(instrument.actions).some((action) =>
|
|
370
|
-
|
|
386
|
+
Object.values(instrument.actions).some((action) => {
|
|
387
|
+
if (
|
|
388
|
+
action.contributionStage &&
|
|
389
|
+
canonicalAccountPath(
|
|
390
|
+
instrument,
|
|
391
|
+
action.contributionStage.accountPath,
|
|
392
|
+
) === account
|
|
393
|
+
) {
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
396
|
+
return (action.moves ?? []).some((step) => {
|
|
371
397
|
if (
|
|
372
398
|
(step.operation !== "internal_transfer.create" &&
|
|
373
399
|
step.operation !== "internal_transfer.reserve") ||
|
|
@@ -381,8 +407,8 @@ function productEscrowAccounts(instrument: FinancialInstrument): Set<string> {
|
|
|
381
407
|
canonicalAccount(instrument, step.bind.destinationAccountId) ===
|
|
382
408
|
account
|
|
383
409
|
);
|
|
384
|
-
})
|
|
385
|
-
),
|
|
410
|
+
});
|
|
411
|
+
}),
|
|
386
412
|
),
|
|
387
413
|
);
|
|
388
414
|
}
|
|
@@ -425,9 +451,37 @@ function effectsByAction(
|
|
|
425
451
|
quotes.map((quote) => [`refs.${quote.chargeRef}`, quote]),
|
|
426
452
|
);
|
|
427
453
|
return new Map(
|
|
428
|
-
Object.entries(instrument.actions).map(([actionName, action]) =>
|
|
429
|
-
|
|
430
|
-
(action.
|
|
454
|
+
Object.entries(instrument.actions).map(([actionName, action]) => {
|
|
455
|
+
const stageEffects: Effect[] = [];
|
|
456
|
+
if (action.contributionStage && instrument.contributions) {
|
|
457
|
+
const stage = action.contributionStage;
|
|
458
|
+
const stageAccount = canonicalAccountPath(
|
|
459
|
+
instrument,
|
|
460
|
+
stage.accountPath,
|
|
461
|
+
);
|
|
462
|
+
if (
|
|
463
|
+
stageAccount &&
|
|
464
|
+
accountsMayAlias(instrument, stageAccount, account)
|
|
465
|
+
) {
|
|
466
|
+
const path = ["actions", actionName, "contributionStage"] as const;
|
|
467
|
+
const totalAmount = `fields.${instrument.contributions.totalField}`;
|
|
468
|
+
if (stage.stage === "fund") {
|
|
469
|
+
stageEffects.push({
|
|
470
|
+
amount: totalAmount,
|
|
471
|
+
kind: "credit",
|
|
472
|
+
path,
|
|
473
|
+
});
|
|
474
|
+
} else if (stage.stage === "refund") {
|
|
475
|
+
stageEffects.push({
|
|
476
|
+
amount: totalAmount,
|
|
477
|
+
kind: "debit",
|
|
478
|
+
path,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const moveEffects = (action.moves ?? []).flatMap((step, stepIndex) => {
|
|
431
485
|
const path = [
|
|
432
486
|
"actions",
|
|
433
487
|
actionName,
|
|
@@ -537,11 +591,95 @@ function effectsByAction(
|
|
|
537
591
|
];
|
|
538
592
|
}
|
|
539
593
|
return [];
|
|
540
|
-
})
|
|
541
|
-
|
|
594
|
+
});
|
|
595
|
+
return [actionName, [...stageEffects, ...moveEffects]];
|
|
596
|
+
}),
|
|
542
597
|
);
|
|
543
598
|
}
|
|
544
599
|
|
|
600
|
+
interface NormalizedPartition {
|
|
601
|
+
readonly total: string;
|
|
602
|
+
readonly pieces: readonly string[];
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function getInstrumentPartitions(
|
|
606
|
+
instrument: FinancialInstrument,
|
|
607
|
+
): readonly NormalizedPartition[] {
|
|
608
|
+
if (!instrument.partitions) return [];
|
|
609
|
+
const partitions: NormalizedPartition[] = [];
|
|
610
|
+
for (const p of instrument.partitions) {
|
|
611
|
+
if (!p.totalField || !p.pieceFields || p.pieceFields.length === 0) continue;
|
|
612
|
+
const total = `fields.${p.totalField}`;
|
|
613
|
+
const pieces = p.pieceFields.map((f) => `fields.${f}`);
|
|
614
|
+
if (new Set(pieces).size !== pieces.length) continue;
|
|
615
|
+
if (pieces.includes(total) && pieces.length > 1) continue;
|
|
616
|
+
partitions.push({ total, pieces });
|
|
617
|
+
}
|
|
618
|
+
return partitions;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function tryPartitionSubstitution(
|
|
622
|
+
instrument: FinancialInstrument,
|
|
623
|
+
balance: Balance,
|
|
624
|
+
holds: Readonly<Record<string, Balance>>,
|
|
625
|
+
exchanged: boolean,
|
|
626
|
+
targetAmount: string,
|
|
627
|
+
): Balance | null {
|
|
628
|
+
if (exchanged) return null;
|
|
629
|
+
if (balance.kind !== "funded") return null;
|
|
630
|
+
if (Object.keys(holds).length > 0) return null;
|
|
631
|
+
|
|
632
|
+
const declaredPartitions = getInstrumentPartitions(instrument);
|
|
633
|
+
if (declaredPartitions.length === 0) return null;
|
|
634
|
+
|
|
635
|
+
for (const pOut of declaredPartitions) {
|
|
636
|
+
const total = pOut.total;
|
|
637
|
+
|
|
638
|
+
let candidateOutPieces: readonly string[] | null = null;
|
|
639
|
+
if (pOut.pieces.includes(targetAmount)) {
|
|
640
|
+
candidateOutPieces = pOut.pieces;
|
|
641
|
+
} else if (targetAmount === total) {
|
|
642
|
+
candidateOutPieces = [total];
|
|
643
|
+
}
|
|
644
|
+
if (!candidateOutPieces) continue;
|
|
645
|
+
|
|
646
|
+
if (candidateOutPieces.length === 0) continue;
|
|
647
|
+
if (new Set(candidateOutPieces).size !== candidateOutPieces.length)
|
|
648
|
+
continue;
|
|
649
|
+
|
|
650
|
+
const incomingCandidates: (readonly string[])[] = [];
|
|
651
|
+
|
|
652
|
+
if (balance.amounts.includes(total)) {
|
|
653
|
+
incomingCandidates.push([total]);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
for (const pIn of declaredPartitions) {
|
|
657
|
+
if (pIn.total !== total) continue;
|
|
658
|
+
if (pIn.pieces.every((piece) => balance.amounts.includes(piece))) {
|
|
659
|
+
incomingCandidates.push(pIn.pieces);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
for (const candidateInPieces of incomingCandidates) {
|
|
664
|
+
if (
|
|
665
|
+
candidateInPieces.length === candidateOutPieces.length &&
|
|
666
|
+
candidateInPieces.every((piece) => candidateOutPieces!.includes(piece))
|
|
667
|
+
) {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
if (
|
|
672
|
+
balance.amounts.length === candidateInPieces.length &&
|
|
673
|
+
candidateInPieces.every((piece) => balance.amounts.includes(piece))
|
|
674
|
+
) {
|
|
675
|
+
return funded(candidateOutPieces);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
|
|
545
683
|
function applyEffects(
|
|
546
684
|
instrument: FinancialInstrument,
|
|
547
685
|
account: string,
|
|
@@ -554,6 +692,7 @@ function applyEffects(
|
|
|
554
692
|
): AccountState {
|
|
555
693
|
let balance = input.balance;
|
|
556
694
|
const holds = { ...input.holds };
|
|
695
|
+
let exchanged = input.exchanged ?? false;
|
|
557
696
|
const accountLabel = formatAccount(account);
|
|
558
697
|
for (const effect of effects) {
|
|
559
698
|
const fail = (message: string): void =>
|
|
@@ -579,8 +718,24 @@ function applyEffects(
|
|
|
579
718
|
}
|
|
580
719
|
if (effect.kind === "incoming_reserve") continue;
|
|
581
720
|
if (effect.kind === "outgoing_reserve") {
|
|
582
|
-
|
|
721
|
+
let held =
|
|
583
722
|
balance.kind === "funded" && balance.amounts.includes(effect.amount);
|
|
723
|
+
if (!held && balance.kind === "funded") {
|
|
724
|
+
const substituted = tryPartitionSubstitution(
|
|
725
|
+
instrument,
|
|
726
|
+
balance,
|
|
727
|
+
holds,
|
|
728
|
+
exchanged,
|
|
729
|
+
effect.amount,
|
|
730
|
+
);
|
|
731
|
+
if (substituted !== null) {
|
|
732
|
+
exchanged = true;
|
|
733
|
+
balance = substituted;
|
|
734
|
+
held =
|
|
735
|
+
balance.kind === "funded" &&
|
|
736
|
+
balance.amounts.includes(effect.amount);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
584
739
|
if (!held) {
|
|
585
740
|
fail(
|
|
586
741
|
`cannot reserve ${effect.amount} from ${accountLabel}; that exact balance is not guaranteed`,
|
|
@@ -676,12 +831,36 @@ function applyEffects(
|
|
|
676
831
|
balance = balance.kind === "empty" ? EMPTY : { kind: "unknown" };
|
|
677
832
|
continue;
|
|
678
833
|
}
|
|
679
|
-
if (balance.kind === "funded"
|
|
680
|
-
|
|
681
|
-
|
|
834
|
+
if (balance.kind === "funded") {
|
|
835
|
+
if (balance.amounts.includes(effect.amount)) {
|
|
836
|
+
balance = funded(withoutPiece(balance.amounts, effect.amount));
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
const substituted = tryPartitionSubstitution(
|
|
840
|
+
instrument,
|
|
841
|
+
balance,
|
|
842
|
+
holds,
|
|
843
|
+
exchanged,
|
|
844
|
+
effect.amount,
|
|
845
|
+
);
|
|
846
|
+
if (substituted !== null) {
|
|
847
|
+
exchanged = true;
|
|
848
|
+
balance = substituted;
|
|
849
|
+
if (
|
|
850
|
+
balance.kind === "funded" &&
|
|
851
|
+
balance.amounts.includes(effect.amount)
|
|
852
|
+
) {
|
|
853
|
+
balance = funded(withoutPiece(balance.amounts, effect.amount));
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
682
857
|
}
|
|
683
858
|
if (balance.kind === "empty") {
|
|
684
|
-
fail(
|
|
859
|
+
fail(
|
|
860
|
+
exchanged
|
|
861
|
+
? `cannot prove ${effect.amount} is available after the substituted partition was spent from ${accountLabel}`
|
|
862
|
+
: `can debit unfunded ${accountLabel}`,
|
|
863
|
+
);
|
|
685
864
|
} else if (balance.kind === "penalty") {
|
|
686
865
|
fail(
|
|
687
866
|
`can debit ${accountLabel} after its refund left only the quoted charge`,
|
|
@@ -693,7 +872,14 @@ function applyEffects(
|
|
|
693
872
|
}
|
|
694
873
|
balance = EMPTY;
|
|
695
874
|
}
|
|
696
|
-
|
|
875
|
+
// A held account permits at most one equal-partition substitution per funded
|
|
876
|
+
// balance, preventing cycles, duplicate minting, and double spending. When an
|
|
877
|
+
// account is completely drained to empty with no active holds, the exchange
|
|
878
|
+
// record resets for any distinct subsequent funding.
|
|
879
|
+
if (balance.kind === "empty" && Object.keys(holds).length === 0) {
|
|
880
|
+
exchanged = false;
|
|
881
|
+
}
|
|
882
|
+
return { balance, holds, exchanged };
|
|
697
883
|
}
|
|
698
884
|
|
|
699
885
|
/**
|
|
@@ -873,7 +1059,14 @@ function canonicalAccount(
|
|
|
873
1059
|
binding: UdlBinding | undefined,
|
|
874
1060
|
): string | undefined {
|
|
875
1061
|
if (binding?.from !== "instance") return undefined;
|
|
876
|
-
|
|
1062
|
+
return canonicalAccountPath(instrument, binding.path);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function canonicalAccountPath(
|
|
1066
|
+
instrument: FinancialInstrument,
|
|
1067
|
+
path: string,
|
|
1068
|
+
): string | undefined {
|
|
1069
|
+
const [root, key, extra] = path.split(".");
|
|
877
1070
|
if (!key || extra !== undefined) return undefined;
|
|
878
1071
|
if (root === "fields") return `field:${key}`;
|
|
879
1072
|
if (root === "refs") return `ref:${key}`;
|
|
@@ -961,6 +1154,7 @@ function stateKey(state: AccountState): string {
|
|
|
961
1154
|
holds: Object.fromEntries(
|
|
962
1155
|
Object.entries(state.holds).sort(([a], [b]) => a.localeCompare(b)),
|
|
963
1156
|
),
|
|
1157
|
+
exchanged: state.exchanged ?? false,
|
|
964
1158
|
});
|
|
965
1159
|
}
|
|
966
1160
|
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
export {
|
|
2
|
+
mapUdlInstrumentReferences,
|
|
3
|
+
referencedUdlInstrumentIds,
|
|
4
|
+
} from "./instrument-references.js";
|
|
1
5
|
export { canonicalizeUdl, parseUdl } from "./parser.js";
|
|
2
6
|
export { canonicalDigest, serializeUdl } from "./canonical.js";
|
|
3
7
|
export { issue, udlDiagnostic, udlDiagnostics } from "./diagnostics.js";
|
|
@@ -52,7 +56,6 @@ export {
|
|
|
52
56
|
udlClauseVocabulary,
|
|
53
57
|
udlDocumentSchema,
|
|
54
58
|
udlInstrumentActionIdSchema,
|
|
55
|
-
udlJourneySchema,
|
|
56
59
|
udlKernelOperationSchema,
|
|
57
60
|
udlProviderFamilyIdSchema,
|
|
58
61
|
udlPublicActionSchema,
|
|
@@ -88,8 +91,6 @@ export type {
|
|
|
88
91
|
UdlLifecycleTransition,
|
|
89
92
|
UdlInstrument,
|
|
90
93
|
UdlInstrumentSubject,
|
|
91
|
-
UdlJourney,
|
|
92
|
-
UdlJourneyStep,
|
|
93
94
|
UdlPayout,
|
|
94
95
|
UdlQuote,
|
|
95
96
|
UdlProviderFamilyId,
|
|
@@ -109,6 +110,7 @@ export {
|
|
|
109
110
|
reconcileExceptionChildProblems,
|
|
110
111
|
openReferenceShapeBudget,
|
|
111
112
|
UdlError,
|
|
113
|
+
instrumentFinanceIssues,
|
|
112
114
|
validateUdl,
|
|
113
115
|
validateUdlJsonSchema,
|
|
114
116
|
validateUdlSchemaValue,
|
|
@@ -118,3 +120,29 @@ export type {
|
|
|
118
120
|
UdlValidationOptions,
|
|
119
121
|
UdlValidationResult,
|
|
120
122
|
} from "./validation.js";
|
|
123
|
+
|
|
124
|
+
export {
|
|
125
|
+
deriveUdlAmount,
|
|
126
|
+
deriveRemainder,
|
|
127
|
+
matchesOrdered,
|
|
128
|
+
matchesDateOrder,
|
|
129
|
+
matchesSchedule,
|
|
130
|
+
isDueBefore,
|
|
131
|
+
planContributions,
|
|
132
|
+
} from "./vocabulary.js";
|
|
133
|
+
export {
|
|
134
|
+
planAllocation,
|
|
135
|
+
planAllocationRefund,
|
|
136
|
+
matchesAllocationConsumption,
|
|
137
|
+
} from "./allocation.js";
|
|
138
|
+
export type { UdlAllocation, UdlAllocate, UdlContributions } from "./schema.js";
|
|
139
|
+
|
|
140
|
+
export type {
|
|
141
|
+
AllocationInput,
|
|
142
|
+
AllocationSlice,
|
|
143
|
+
AllocationBalance,
|
|
144
|
+
AllocationPosting,
|
|
145
|
+
AllocationCancellation,
|
|
146
|
+
} from "./allocation.js";
|
|
147
|
+
|
|
148
|
+
export { distributeReceiptAmounts } from "./distribution.js";
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
udlDocumentSchema,
|
|
4
|
+
type UdlDocument,
|
|
5
|
+
type UdlInstrument,
|
|
6
|
+
} from "./schema.js";
|
|
7
|
+
|
|
8
|
+
type Schema = Record<string, unknown>;
|
|
9
|
+
const object = (value: unknown): value is Schema =>
|
|
10
|
+
value !== null && typeof value === "object" && !Array.isArray(value);
|
|
11
|
+
let documentSchema: Schema | undefined;
|
|
12
|
+
|
|
13
|
+
/** Visit identities through schema annotations, never through user JSON keys. */
|
|
14
|
+
export function mapUdlInstrumentReferences(
|
|
15
|
+
document: UdlDocument,
|
|
16
|
+
map: (id: string) => string,
|
|
17
|
+
): UdlDocument {
|
|
18
|
+
const root = (documentSchema ??= z.toJSONSchema(udlDocumentSchema));
|
|
19
|
+
const result = structuredClone(document);
|
|
20
|
+
const locations = new Map<
|
|
21
|
+
string,
|
|
22
|
+
{ parent: Schema | unknown[]; key: string | number; kind: string }
|
|
23
|
+
>();
|
|
24
|
+
function visit(
|
|
25
|
+
schema: unknown,
|
|
26
|
+
value: unknown,
|
|
27
|
+
parent: Schema | unknown[],
|
|
28
|
+
key: string | number,
|
|
29
|
+
path: readonly (string | number)[],
|
|
30
|
+
): void {
|
|
31
|
+
if (!object(schema)) return;
|
|
32
|
+
if (typeof schema.$ref === "string") {
|
|
33
|
+
if (!schema.$ref.startsWith("#/"))
|
|
34
|
+
throw new Error("UDL reference schema must be local");
|
|
35
|
+
let target: unknown = root;
|
|
36
|
+
for (const part of schema.$ref.slice(2).split("/")) {
|
|
37
|
+
target = object(target)
|
|
38
|
+
? target[part.replaceAll("~1", "/").replaceAll("~0", "~")]
|
|
39
|
+
: undefined;
|
|
40
|
+
}
|
|
41
|
+
visit(target, value, parent, key, path);
|
|
42
|
+
}
|
|
43
|
+
const kind = schema["x-udl-reference"];
|
|
44
|
+
if (typeof kind === "string" && typeof value === "string")
|
|
45
|
+
locations.set(JSON.stringify(path), { parent, key, kind });
|
|
46
|
+
for (const union of [schema.anyOf, schema.oneOf, schema.allOf])
|
|
47
|
+
if (Array.isArray(union))
|
|
48
|
+
for (const branch of union) visit(branch, value, parent, key, path);
|
|
49
|
+
if (Array.isArray(value)) {
|
|
50
|
+
value.forEach((entry, index) =>
|
|
51
|
+
visit(schema.items, entry, value, index, [...path, index]),
|
|
52
|
+
);
|
|
53
|
+
} else if (object(value)) {
|
|
54
|
+
const properties = object(schema.properties) ? schema.properties : {};
|
|
55
|
+
for (const [name, entry] of Object.entries(value))
|
|
56
|
+
visit(
|
|
57
|
+
properties[name] ?? schema.additionalProperties,
|
|
58
|
+
entry,
|
|
59
|
+
value,
|
|
60
|
+
name,
|
|
61
|
+
[...path, name],
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
visit(root, result, [result], 0, []);
|
|
66
|
+
for (const { parent, key, kind } of locations.values()) {
|
|
67
|
+
const holder = parent as Record<string | number, unknown>;
|
|
68
|
+
const value = holder[key] as string;
|
|
69
|
+
if (kind === "instrument") holder[key] = map(value);
|
|
70
|
+
else if (kind === "instrument_event") {
|
|
71
|
+
const separator = value.indexOf(".");
|
|
72
|
+
holder[key] = map(value.slice(0, separator)) + value.slice(separator);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function referencedUdlInstrumentIds(
|
|
79
|
+
instrument: UdlInstrument,
|
|
80
|
+
): readonly string[] {
|
|
81
|
+
const found = new Set<string>();
|
|
82
|
+
mapUdlInstrumentReferences(
|
|
83
|
+
{
|
|
84
|
+
udl: 1,
|
|
85
|
+
version: 1,
|
|
86
|
+
product: "references",
|
|
87
|
+
title: "References",
|
|
88
|
+
subjects: [],
|
|
89
|
+
instruments: [instrument],
|
|
90
|
+
},
|
|
91
|
+
(id) => {
|
|
92
|
+
found.add(id);
|
|
93
|
+
return id;
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
found.delete(instrument.id);
|
|
97
|
+
return [...found];
|
|
98
|
+
}
|
package/src/limits.ts
CHANGED
|
@@ -11,7 +11,6 @@ export const UDL_LIMITS = Object.freeze({
|
|
|
11
11
|
maxActionLeaves: 256,
|
|
12
12
|
maxDepth: 24,
|
|
13
13
|
maxKeyLength: 128,
|
|
14
|
-
// The 33-instrument catalog includes its executable authored journeys.
|
|
15
14
|
maxNodes: 20_000,
|
|
16
15
|
maxPatternLength: 320,
|
|
17
16
|
/**
|
|
@@ -25,13 +24,10 @@ export const UDL_LIMITS = Object.freeze({
|
|
|
25
24
|
maxPatternPaths: 4_096,
|
|
26
25
|
/**
|
|
27
26
|
* Reference-shape classifications one `ReferenceShapeBudget` may buy. Each
|
|
28
|
-
* one
|
|
29
|
-
*
|
|
27
|
+
* one classifies canonical reference pattern text without compiling a validator.
|
|
28
|
+
* This safety net covers a catalogue plus hundreds of authored instruments.
|
|
30
29
|
*/
|
|
31
|
-
maxSchemaProbes:
|
|
32
|
-
// The complete catalog now carries executable authored journeys beside the
|
|
33
|
-
// instrument mechanics, so its bounded source and string budgets include
|
|
34
|
-
// that contract-owned corpus.
|
|
30
|
+
maxSchemaProbes: 131_072,
|
|
35
31
|
maxSourceBytes: 1_024 * 1_024,
|
|
36
32
|
maxStringLength: 2_048,
|
|
37
33
|
maxTotalStringLength: 512 * 1_024,
|
package/src/reference.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Read the sealed ID grammar without executing document-authored patterns. */
|
|
2
|
+
export function referencePatternPrefix(
|
|
3
|
+
schema: Readonly<Record<string, unknown>>,
|
|
4
|
+
): string | undefined {
|
|
5
|
+
if (schema.type !== "string" || typeof schema.pattern !== "string")
|
|
6
|
+
return undefined;
|
|
7
|
+
const match = /^\^([a-z]{2,8})_\(sandbox\|live\)_\[a-z0-9\]\{8,64\}\$$/.exec(
|
|
8
|
+
schema.pattern,
|
|
9
|
+
);
|
|
10
|
+
const prefix = match?.[1];
|
|
11
|
+
if (!prefix) return undefined;
|
|
12
|
+
const min = typeof schema.minLength === "number" ? schema.minLength : 0;
|
|
13
|
+
const max =
|
|
14
|
+
typeof schema.maxLength === "number" ? schema.maxLength : Infinity;
|
|
15
|
+
if (min > prefix.length + 73 || max < prefix.length + 14 || min > max)
|
|
16
|
+
return undefined;
|
|
17
|
+
const validId = (value: unknown): boolean => {
|
|
18
|
+
if (typeof value !== "string") return false;
|
|
19
|
+
const id = /^([a-z]{2,8})_(sandbox|live)_([a-z0-9]{8,64})$/.exec(value);
|
|
20
|
+
return id?.[1] === prefix && value.length >= min && value.length <= max;
|
|
21
|
+
};
|
|
22
|
+
if (schema.const !== undefined && !validId(schema.const)) return undefined;
|
|
23
|
+
if (
|
|
24
|
+
Array.isArray(schema.enum) &&
|
|
25
|
+
(!schema.enum.length ||
|
|
26
|
+
!schema.enum.every(validId) ||
|
|
27
|
+
(schema.const !== undefined && !schema.enum.includes(schema.const)))
|
|
28
|
+
)
|
|
29
|
+
return undefined;
|
|
30
|
+
return prefix;
|
|
31
|
+
}
|