@hyperscale0/udl 2.0.2 → 2.0.3

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.
@@ -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.
@@ -1,4 +1,4 @@
1
- <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.0.2. Edit the source, not llms-full.txt. -->
1
+ <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.0.3. 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
@@ -1,4 +1,4 @@
1
- <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.0.2. Edit the source, not llms.txt. -->
1
+ <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.0.3. Edit the source, not llms.txt. -->
2
2
 
3
3
  # UDL documentation
4
4
 
@@ -1,4 +1,4 @@
1
- <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.0.2. Edit the source, not clauses.md. -->
1
+ <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.0.3. Edit the source, not clauses.md. -->
2
2
 
3
3
  # Clause reference
4
4
 
@@ -1,4 +1,4 @@
1
- <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.0.2. Edit the source, not cli.md. -->
1
+ <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.0.3. Edit the source, not cli.md. -->
2
2
 
3
3
  # Command reference
4
4
 
@@ -1,4 +1,4 @@
1
- <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.0.2. Edit the source, not diagnostics.md. -->
1
+ <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.0.3. Edit the source, not diagnostics.md. -->
2
2
 
3
3
  # Diagnostic reference
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperscale0/udl",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
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": "70ddac0ae5599ca61c7951b027befbeb6eea9f96"
87
+ "gitHead": "d9200bc6856f30811e8aa2888c44fdd045424120"
88
88
  }
@@ -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
- if (trackedAccounts.size === 0) return [];
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