@hyperscale0/udl 2.0.2 → 2.0.4
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 +12 -0
- package/dist/finance.d.ts +1 -0
- package/dist/finance.d.ts.map +1 -1
- package/dist/finance.js +42 -4
- package/dist/finance.js.map +1 -1
- package/dist/schema.d.ts +36 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +7 -0
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +66 -0
- package/dist/validation.js.map +1 -1
- package/docs/guide/04-fees-and-remainder.md +21 -0
- package/docs/llms-full.txt +22 -1
- package/docs/llms.txt +1 -1
- package/docs/reference/clauses.md +1 -1
- package/docs/reference/cli.md +1 -1
- package/docs/reference/diagnostics.md +1 -1
- package/package.json +2 -2
- package/spec/udl.schema.json +4 -0
- package/src/finance.ts +76 -9
- package/src/schema.ts +9 -0
- package/src/validation.ts +94 -0
|
@@ -13,3 +13,24 @@ Fee rules derive named amounts from a stored base amount. Partitions state how c
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Computed money has one author. A remainder, signed sum, distribution, quote, or derived amount must not compete with another clause for the same field or ref. A money move consumes the computed value once. The validator rejects missing sources, duplicate outputs, and terminal paths that strand funded value.
|
|
16
|
+
|
|
17
|
+
## Retained charges
|
|
18
|
+
|
|
19
|
+
A quote may declare `chargeRetainedBy` with a role (`payer`, `beneficiary`, or `subjectHolder`). The named role keeps the quoted charge instead of moving it through an escrow payout action. The role must be declared under `parties`, its account field must be required and frozen by `fixes`, and the committing refund transfer must source funds directly from that account.
|
|
20
|
+
|
|
21
|
+
```jsonc
|
|
22
|
+
{
|
|
23
|
+
"quote": {
|
|
24
|
+
"baseField": "premiumAmount",
|
|
25
|
+
"chargeRef": "unwindPenalty",
|
|
26
|
+
"chargeRetainedBy": "beneficiary",
|
|
27
|
+
"charges": [{ "bps": 1000 }],
|
|
28
|
+
"expires": { "offset": "PT15M" },
|
|
29
|
+
"fixes": ["insurerAccountId", "policyholderAccountId", "premiumAmount"],
|
|
30
|
+
"netDestinationField": "policyholderAccountId",
|
|
31
|
+
"netRef": "policyRefund",
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The retained remainder stays in the source account and cannot be spent by subsequent instrument actions. Actions cannot consume the charge reference, and no self-transfer is permitted.
|
package/docs/llms-full.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.0.
|
|
1
|
+
<!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.0.4. Edit the source, not llms-full.txt. -->
|
|
2
2
|
|
|
3
3
|
# UDL complete reference
|
|
4
4
|
|
|
@@ -124,6 +124,27 @@ Fee rules derive named amounts from a stored base amount. Partitions state how c
|
|
|
124
124
|
|
|
125
125
|
Computed money has one author. A remainder, signed sum, distribution, quote, or derived amount must not compete with another clause for the same field or ref. A money move consumes the computed value once. The validator rejects missing sources, duplicate outputs, and terminal paths that strand funded value.
|
|
126
126
|
|
|
127
|
+
## Retained charges
|
|
128
|
+
|
|
129
|
+
A quote may declare `chargeRetainedBy` with a role (`payer`, `beneficiary`, or `subjectHolder`). The named role keeps the quoted charge instead of moving it through an escrow payout action. The role must be declared under `parties`, its account field must be required and frozen by `fixes`, and the committing refund transfer must source funds directly from that account.
|
|
130
|
+
|
|
131
|
+
```jsonc
|
|
132
|
+
{
|
|
133
|
+
"quote": {
|
|
134
|
+
"baseField": "premiumAmount",
|
|
135
|
+
"chargeRef": "unwindPenalty",
|
|
136
|
+
"chargeRetainedBy": "beneficiary",
|
|
137
|
+
"charges": [{ "bps": 1000 }],
|
|
138
|
+
"expires": { "offset": "PT15M" },
|
|
139
|
+
"fixes": ["insurerAccountId", "policyholderAccountId", "premiumAmount"],
|
|
140
|
+
"netDestinationField": "policyholderAccountId",
|
|
141
|
+
"netRef": "policyRefund",
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
The retained remainder stays in the source account and cannot be spent by subsequent instrument actions. Actions cannot consume the charge reference, and no self-transfer is permitted.
|
|
147
|
+
|
|
127
148
|
<!-- source: guide/05-checks-updates-dials.md -->
|
|
128
149
|
|
|
129
150
|
# Checks, updates, and dials
|
package/docs/llms.txt
CHANGED
package/docs/reference/cli.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperscale0/udl",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "The Universal Domain Language: format spec, parser, validator, canonical serializer, and evolution diff.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"udl",
|
|
@@ -84,5 +84,5 @@
|
|
|
84
84
|
"Amir Ayub",
|
|
85
85
|
"Sara AlBakaawi"
|
|
86
86
|
],
|
|
87
|
-
"gitHead": "
|
|
87
|
+
"gitHead": "26f31e276e500ee9e6e9a3e9c025c13f7b06d8d9"
|
|
88
88
|
}
|
package/spec/udl.schema.json
CHANGED
|
@@ -1302,6 +1302,10 @@
|
|
|
1302
1302
|
"type": "string",
|
|
1303
1303
|
"pattern": "^[a-z][A-Za-z0-9]*$"
|
|
1304
1304
|
},
|
|
1305
|
+
"chargeRetainedBy": {
|
|
1306
|
+
"type": "string",
|
|
1307
|
+
"enum": ["payer", "beneficiary", "subjectHolder"]
|
|
1308
|
+
},
|
|
1305
1309
|
"charges": {
|
|
1306
1310
|
"minItems": 1,
|
|
1307
1311
|
"type": "array",
|
package/src/finance.ts
CHANGED
|
@@ -47,6 +47,11 @@ interface FinancialInstrument {
|
|
|
47
47
|
| {
|
|
48
48
|
readonly baseField: string;
|
|
49
49
|
readonly chargeRef: string;
|
|
50
|
+
readonly chargeRetainedBy?:
|
|
51
|
+
| "payer"
|
|
52
|
+
| "beneficiary"
|
|
53
|
+
| "subjectHolder"
|
|
54
|
+
| undefined;
|
|
50
55
|
readonly charges: readonly { readonly bps: number }[];
|
|
51
56
|
readonly netRef: string;
|
|
52
57
|
}
|
|
@@ -65,6 +70,11 @@ interface FinancialInstrument {
|
|
|
65
70
|
interface QuoteFacts {
|
|
66
71
|
readonly baseField: string;
|
|
67
72
|
readonly chargeRef: string;
|
|
73
|
+
readonly chargeRetainedBy?:
|
|
74
|
+
| "payer"
|
|
75
|
+
| "beneficiary"
|
|
76
|
+
| "subjectHolder"
|
|
77
|
+
| undefined;
|
|
68
78
|
readonly chargeCanBeNonzero: boolean;
|
|
69
79
|
readonly commit: string;
|
|
70
80
|
readonly netRef: string;
|
|
@@ -172,7 +182,15 @@ export function analyzeInstrumentFinance(
|
|
|
172
182
|
trackedAccounts.add(source);
|
|
173
183
|
}
|
|
174
184
|
const refundSourceAccounts = new Set(refundSources.values());
|
|
175
|
-
|
|
185
|
+
for (const quote of quotes) {
|
|
186
|
+
validateChargePayout(
|
|
187
|
+
instrument,
|
|
188
|
+
quote,
|
|
189
|
+
refundSources.get(quote.commit),
|
|
190
|
+
add,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
if (trackedAccounts.size === 0) return issues;
|
|
176
194
|
if (trackedAccounts.size > UDL_LIMITS.financeAccounts) {
|
|
177
195
|
add(
|
|
178
196
|
["actions"],
|
|
@@ -183,14 +201,6 @@ export function analyzeInstrumentFinance(
|
|
|
183
201
|
}
|
|
184
202
|
|
|
185
203
|
const reservations = reservationsByKey(instrument);
|
|
186
|
-
for (const quote of quotes) {
|
|
187
|
-
validateChargePayout(
|
|
188
|
-
instrument,
|
|
189
|
-
quote,
|
|
190
|
-
refundSources.get(quote.commit),
|
|
191
|
-
add,
|
|
192
|
-
);
|
|
193
|
-
}
|
|
194
204
|
|
|
195
205
|
let pathVariants = 0;
|
|
196
206
|
let work = 0;
|
|
@@ -720,6 +730,7 @@ function quoteFacts(
|
|
|
720
730
|
declaresChargePayout ||
|
|
721
731
|
quote.charges.some((tier) => tier.bps > 0),
|
|
722
732
|
chargeRef: quote.chargeRef,
|
|
733
|
+
chargeRetainedBy: quote.chargeRetainedBy,
|
|
723
734
|
commit,
|
|
724
735
|
netRef: quote.netRef,
|
|
725
736
|
quoting,
|
|
@@ -750,6 +761,62 @@ function validateChargePayout(
|
|
|
750
761
|
),
|
|
751
762
|
),
|
|
752
763
|
);
|
|
764
|
+
|
|
765
|
+
if (quote.chargeRetainedBy !== undefined) {
|
|
766
|
+
const role = quote.chargeRetainedBy;
|
|
767
|
+
const partyField = Object.hasOwn(instrument.parties ?? {}, role)
|
|
768
|
+
? instrument.parties?.[
|
|
769
|
+
role as keyof NonNullable<FinancialInstrument["parties"]>
|
|
770
|
+
]
|
|
771
|
+
: undefined;
|
|
772
|
+
if (!partyField) {
|
|
773
|
+
add(
|
|
774
|
+
["actions", quote.quoting, "quote", "chargeRetainedBy"],
|
|
775
|
+
`quoting action ${quote.quoting} retains charge by undeclared party role ${role}`,
|
|
776
|
+
);
|
|
777
|
+
} else {
|
|
778
|
+
const expectedSource = `field:${partyField}`;
|
|
779
|
+
if (refundSource === undefined) {
|
|
780
|
+
add(
|
|
781
|
+
["actions", quote.commit, "moves"],
|
|
782
|
+
`commit action ${quote.commit} refund source cannot be resolved to charge-retaining party field ${partyField}`,
|
|
783
|
+
);
|
|
784
|
+
} else if (refundSource !== expectedSource) {
|
|
785
|
+
if (refundSource.startsWith("ref:")) {
|
|
786
|
+
add(
|
|
787
|
+
["actions", quote.commit, "moves"],
|
|
788
|
+
`commit action ${quote.commit} refund source cannot be a product escrow ref for a retained quote`,
|
|
789
|
+
);
|
|
790
|
+
} else {
|
|
791
|
+
add(
|
|
792
|
+
["actions", quote.commit, "moves"],
|
|
793
|
+
`commit action ${quote.commit} refund source ${refundSource} does not match charge-retaining party field ${partyField}`,
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
if (uses.length > 0) {
|
|
799
|
+
add(
|
|
800
|
+
["actions"],
|
|
801
|
+
`${chargePath} is retained by ${role} and cannot be consumed by an action`,
|
|
802
|
+
);
|
|
803
|
+
for (const use of uses) {
|
|
804
|
+
add(
|
|
805
|
+
[
|
|
806
|
+
"actions",
|
|
807
|
+
use.actionName,
|
|
808
|
+
"moves",
|
|
809
|
+
use.stepIndex,
|
|
810
|
+
"bind",
|
|
811
|
+
use.target,
|
|
812
|
+
],
|
|
813
|
+
`${chargePath} is retained by ${role} and cannot be consumed by an action`,
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
753
820
|
if (quote.chargeCanBeNonzero && uses.length !== 1) {
|
|
754
821
|
add(
|
|
755
822
|
["actions"],
|
package/src/schema.ts
CHANGED
|
@@ -237,6 +237,13 @@ const udlQuoteChargeSchema = z.strictObject({
|
|
|
237
237
|
withinOffset: nonEmptyTextSchema.optional(),
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
+
export const udlQuoteRetainedRoleSchema = z.enum([
|
|
241
|
+
"payer",
|
|
242
|
+
"beneficiary",
|
|
243
|
+
"subjectHolder",
|
|
244
|
+
]);
|
|
245
|
+
export type UdlQuoteRetainedRole = z.infer<typeof udlQuoteRetainedRoleSchema>;
|
|
246
|
+
|
|
240
247
|
// A priced, expiring offer. The quoting action splits `baseField` into a charge
|
|
241
248
|
// and a net at its own clock, freezes the fields the price was read from, and
|
|
242
249
|
// stamps an expiry. Only the paired `commit` action may spend the offer, and
|
|
@@ -246,6 +253,8 @@ const udlQuoteShape = {
|
|
|
246
253
|
anchorField: udlFieldNameSchema.optional(),
|
|
247
254
|
baseField: udlFieldNameSchema,
|
|
248
255
|
chargeRef: udlFieldNameSchema,
|
|
256
|
+
/** Party role that keeps the quoted charge instead of moving it through an escrow payout action. */
|
|
257
|
+
chargeRetainedBy: udlQuoteRetainedRoleSchema.optional(),
|
|
249
258
|
charges: z.array(udlQuoteChargeSchema).min(1),
|
|
250
259
|
/** Fixed ISO-8601 duration from the quoting action, or a stored deadline. */
|
|
251
260
|
expires: z.union([
|
package/src/validation.ts
CHANGED
|
@@ -4262,6 +4262,69 @@ function validateQuoteCommit(
|
|
|
4262
4262
|
"UDL5006",
|
|
4263
4263
|
);
|
|
4264
4264
|
}
|
|
4265
|
+
if (quote.chargeRetainedBy !== undefined) {
|
|
4266
|
+
const retainedRole = quote.chargeRetainedBy;
|
|
4267
|
+
// Own keys only: a role named after a prototype member is undeclared.
|
|
4268
|
+
const partyField = Object.hasOwn(instrument.parties ?? {}, retainedRole)
|
|
4269
|
+
? instrument.parties?.[retainedRole]
|
|
4270
|
+
: undefined;
|
|
4271
|
+
if (!partyField) {
|
|
4272
|
+
add(
|
|
4273
|
+
[...quoteBase, "chargeRetainedBy"],
|
|
4274
|
+
`quoting action ${actionName} retains charge by undeclared party role ${retainedRole}`,
|
|
4275
|
+
"UDL5006",
|
|
4276
|
+
);
|
|
4277
|
+
} else {
|
|
4278
|
+
const partySchema = instrument.fields[partyField];
|
|
4279
|
+
if (!partySchema || !references.accepts(partySchema, "acct")) {
|
|
4280
|
+
add(
|
|
4281
|
+
[...quoteBase, "chargeRetainedBy"],
|
|
4282
|
+
`charge-retaining party field ${partyField} must be an account field`,
|
|
4283
|
+
"UDL5006",
|
|
4284
|
+
);
|
|
4285
|
+
}
|
|
4286
|
+
if (!instrument.required.includes(partyField)) {
|
|
4287
|
+
add(
|
|
4288
|
+
[...quoteBase, "chargeRetainedBy"],
|
|
4289
|
+
`charge-retaining party field ${partyField} must be required`,
|
|
4290
|
+
"UDL5006",
|
|
4291
|
+
);
|
|
4292
|
+
}
|
|
4293
|
+
if (!quote.fixes.includes(partyField)) {
|
|
4294
|
+
add(
|
|
4295
|
+
[...quoteBase, "chargeRetainedBy"],
|
|
4296
|
+
`quoting action ${actionName} must freeze its charge-retaining party field ${partyField}`,
|
|
4297
|
+
"UDL5006",
|
|
4298
|
+
);
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
const chargePath = `refs.${quote.chargeRef}`;
|
|
4302
|
+
for (const [consumerName, candidateAction] of Object.entries(
|
|
4303
|
+
instrument.actions,
|
|
4304
|
+
)) {
|
|
4305
|
+
for (const [moveIndex, move] of (
|
|
4306
|
+
candidateAction.moves ?? []
|
|
4307
|
+
).entries()) {
|
|
4308
|
+
for (const [target, binding] of Object.entries(move.bind)) {
|
|
4309
|
+
if (binding.from === "instance" && binding.path === chargePath) {
|
|
4310
|
+
add(
|
|
4311
|
+
[
|
|
4312
|
+
...base,
|
|
4313
|
+
"actions",
|
|
4314
|
+
consumerName,
|
|
4315
|
+
"moves",
|
|
4316
|
+
moveIndex,
|
|
4317
|
+
"bind",
|
|
4318
|
+
target,
|
|
4319
|
+
],
|
|
4320
|
+
`charge ${chargePath} is retained by ${retainedRole} and cannot be consumed by an action`,
|
|
4321
|
+
"UDL5006",
|
|
4322
|
+
);
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
}
|
|
4265
4328
|
}
|
|
4266
4329
|
|
|
4267
4330
|
const seededBy = new Map<string, string>();
|
|
@@ -4342,6 +4405,37 @@ function validateQuoteCommit(
|
|
|
4342
4405
|
"UDL5006",
|
|
4343
4406
|
);
|
|
4344
4407
|
}
|
|
4408
|
+
if (quote.chargeRetainedBy !== undefined) {
|
|
4409
|
+
const partyField = instrument.parties?.[quote.chargeRetainedBy];
|
|
4410
|
+
let sourceField: string | undefined;
|
|
4411
|
+
if (sourceBinding?.from === "instance") {
|
|
4412
|
+
if (sourceBinding.path.startsWith("fields.")) {
|
|
4413
|
+
sourceField = sourceBinding.path.slice("fields.".length);
|
|
4414
|
+
} else if (sourceBinding.path.startsWith("party.")) {
|
|
4415
|
+
const role = sourceBinding.path.slice("party.".length);
|
|
4416
|
+
sourceField = instrument.parties?.[role];
|
|
4417
|
+
}
|
|
4418
|
+
}
|
|
4419
|
+
if (
|
|
4420
|
+
sourceBinding?.from !== "instance" ||
|
|
4421
|
+
!sourceField ||
|
|
4422
|
+
sourceField !== partyField
|
|
4423
|
+
) {
|
|
4424
|
+
add(
|
|
4425
|
+
[
|
|
4426
|
+
...base,
|
|
4427
|
+
"actions",
|
|
4428
|
+
actionName,
|
|
4429
|
+
"moves",
|
|
4430
|
+
0,
|
|
4431
|
+
"bind",
|
|
4432
|
+
"sourceAccountId",
|
|
4433
|
+
],
|
|
4434
|
+
`commit action ${actionName} refund source must come from charge-retaining party field ${partyField ?? quote.chargeRetainedBy}`,
|
|
4435
|
+
"UDL5006",
|
|
4436
|
+
);
|
|
4437
|
+
}
|
|
4438
|
+
}
|
|
4345
4439
|
}
|
|
4346
4440
|
}
|
|
4347
4441
|
|