@hyperscale0/udl 1.0.0-alpha.4 → 1.0.0-beta.1

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/src/schema.ts CHANGED
@@ -18,12 +18,22 @@ const udlFieldNameSchema = z
18
18
  .string()
19
19
  .regex(camelCasePattern, "must be a camelCase field name");
20
20
 
21
+ export const udlPublicIntentSchema = z
22
+ .string()
23
+ .regex(camelCasePattern, "must be a camelCase public intent name");
24
+
21
25
  const nonEmptyTextSchema = z
22
26
  .string()
23
27
  .refine((value) => value.trim().length > 0, "must not be blank");
24
28
  const fieldPathSchema = z
25
29
  .string()
26
30
  .regex(fieldPathPattern, "must be a dot-separated camelCase field path");
31
+ const instanceValuePathSchema = z
32
+ .string()
33
+ .regex(
34
+ /^(?:fields|refs)\.[a-z][A-Za-z0-9]*$/,
35
+ "must name one declared fields.* or refs.* value",
36
+ );
27
37
  const jsonObjectSchema = z.record(z.string(), z.json());
28
38
  const stringMapSchema = z.record(nonEmptyTextSchema, nonEmptyTextSchema);
29
39
 
@@ -139,6 +149,7 @@ const udlDeadlineSchema = z.strictObject({
139
149
 
140
150
  const udlSetsAtSchema = z.strictObject({
141
151
  field: udlFieldNameSchema,
152
+ marker: z.literal(true).optional(),
142
153
  offset: nonEmptyTextSchema,
143
154
  });
144
155
 
@@ -215,6 +226,15 @@ const udlAggregateConditionSchema = z.strictObject({
215
226
  statuses: z.array(udlSnakeCaseSchema).min(1),
216
227
  });
217
228
 
229
+ const udlExposureRequirementSchema = z.strictObject({
230
+ amountField: udlFieldNameSchema,
231
+ anchorField: udlFieldNameSchema,
232
+ capField: udlFieldNameSchema,
233
+ capOnAnchor: z.literal(true).optional(),
234
+ childNounId: udlSnakeCaseSchema,
235
+ statuses: z.array(udlSnakeCaseSchema).min(1),
236
+ });
237
+
218
238
  // The tenant-backend decision port: the verb's caller asserts the acting
219
239
  // party, checked at admission against the noun's party bindings for the
220
240
  // allowed roles.
@@ -224,6 +244,49 @@ const udlPortSchema = z.strictObject({
224
244
  .min(1),
225
245
  });
226
246
 
247
+ const udlPayoutSchema = z.strictObject({
248
+ amount: instanceValuePathSchema,
249
+ beneficiaryField: udlFieldNameSchema,
250
+ beneficiaryPartyField: udlFieldNameSchema,
251
+ capture: udlFieldNameSchema,
252
+ currencyField: udlFieldNameSchema,
253
+ sourceAccountField: udlFieldNameSchema,
254
+ speed: z.literal("standard"),
255
+ });
256
+
257
+ const udlRequiresSettlementSchema = z.strictObject({
258
+ capture: udlFieldNameSchema,
259
+ payoutRef: udlFieldNameSchema,
260
+ });
261
+
262
+ const udlSignedSumSourceSchema = z.strictObject({
263
+ amountField: udlFieldNameSchema,
264
+ nounId: udlSnakeCaseSchema,
265
+ refField: udlFieldNameSchema,
266
+ sign: z.enum(["add", "subtract"]),
267
+ statuses: z.array(udlSnakeCaseSchema).min(1),
268
+ subtotalRef: udlFieldNameSchema,
269
+ });
270
+
271
+ const udlSignedSumSchema = z.strictObject({
272
+ amountRef: udlFieldNameSchema,
273
+ onNegative: z.literal("refuse"),
274
+ onZero: z.enum(["refuse", "skip_steps"]),
275
+ sources: z.array(udlSignedSumSourceSchema).min(1),
276
+ });
277
+
278
+ const udlDistributeSchema = z.strictObject({
279
+ amountRef: udlFieldNameSchema,
280
+ onZero: z.enum(["refuse", "skip_steps"]),
281
+ pool: z.strictObject({
282
+ from: z.literal("parent"),
283
+ path: fieldPathSchema,
284
+ }),
285
+ refField: udlFieldNameSchema,
286
+ statuses: z.array(udlSnakeCaseSchema).min(1),
287
+ weightField: udlFieldNameSchema,
288
+ });
289
+
227
290
  // A same-instance conservation law: the piece fields (money, minor units)
228
291
  // must sum exactly to the total field; admission refuses a create whose
229
292
  // pieces diverge.
@@ -232,9 +295,21 @@ const udlPartitionSchema = z.strictObject({
232
295
  totalField: udlFieldNameSchema,
233
296
  });
234
297
 
298
+ const udlDerivedAmountSchema = z.strictObject({
299
+ field: udlFieldNameSchema,
300
+ rounding: z.literal("floor"),
301
+ rule: z.strictObject({
302
+ bps: z.number().int().min(1).max(9_999),
303
+ kind: z.literal("percentage_of"),
304
+ }),
305
+ sourceField: udlFieldNameSchema,
306
+ });
307
+
235
308
  const udlVerbSchema = z.strictObject({
309
+ captureInput: z.record(udlFieldNameSchema, udlFieldNameSchema).optional(),
236
310
  deadline: udlDeadlineSchema.optional(),
237
311
  decision: udlDecisionSchema.optional(),
312
+ distribute: udlDistributeSchema.optional(),
238
313
  description: nonEmptyTextSchema.optional(),
239
314
  due: udlDueSchema.optional(),
240
315
  earnable: z.boolean().optional(),
@@ -242,11 +317,20 @@ const udlVerbSchema = z.strictObject({
242
317
  examples: z.array(udlExampleSchema).min(1).optional(),
243
318
  input: jsonObjectSchema.optional(),
244
319
  moves: z.array(udlMoveSchema).default([]),
320
+ payout: udlPayoutSchema.optional(),
245
321
  port: udlPortSchema.optional(),
322
+ publicIntent: udlPublicIntentSchema
323
+ .optional()
324
+ .describe(
325
+ "Author-approved public intent name; the containing verb key remains the lifecycle and execution identity",
326
+ ),
246
327
  requiresAggregate: z.array(udlAggregateConditionSchema).min(1).optional(),
247
328
  requiresDrainedAccount: z.strictObject({ path: fieldPathSchema }).optional(),
329
+ requiresExposure: z.array(udlExposureRequirementSchema).min(1).optional(),
248
330
  requiresRefs: z.array(udlGateSchema).min(1).optional(),
331
+ requiresSettlement: udlRequiresSettlementSchema.optional(),
249
332
  setsAt: udlSetsAtSchema.optional(),
333
+ signedSum: udlSignedSumSchema.optional(),
250
334
  steps: z.array(udlStepSchema),
251
335
  summary: nonEmptyTextSchema,
252
336
  });
@@ -280,8 +364,10 @@ const udlAggregateSchema = z.union([
280
364
 
281
365
  const udlNounSchema = z.strictObject({
282
366
  aggregateInvariants: z.array(udlAggregateSchema).min(1).optional(),
367
+ computedMoneyRefs: z.array(udlFieldNameSchema).min(1).optional(),
283
368
  description: nonEmptyTextSchema.optional(),
284
369
  distinctParties: z.literal(true).optional(),
370
+ derivedAmounts: z.array(udlDerivedAmountSchema).min(1).max(4).optional(),
285
371
  fields: z.record(udlFieldNameSchema, jsonObjectSchema),
286
372
  id: udlSnakeCaseSchema,
287
373
  idPrefix: z
@@ -305,7 +391,7 @@ const udlNounSchema = z.strictObject({
305
391
  verbs: z.record(udlVerbNameSchema, udlVerbSchema),
306
392
  });
307
393
 
308
- export const udlDocumentSchema = z.strictObject({
394
+ const udlDocumentShapeSchema = z.strictObject({
309
395
  nouns: z.array(udlNounSchema).min(1),
310
396
  product: udlSnakeCaseSchema,
311
397
  subjects: z.array(udlSubjectSchema),
@@ -314,6 +400,155 @@ export const udlDocumentSchema = z.strictObject({
314
400
  version: z.number().int().positive(),
315
401
  });
316
402
 
403
+ type ParsedNoun = z.infer<typeof udlNounSchema>;
404
+
405
+ const moneyPatterns = new Set(["^[1-9][0-9]{0,17}$", "^(0|[1-9][0-9]{0,17})$"]);
406
+
407
+ function isMoneyField(
408
+ schema: Readonly<Record<string, unknown>> | undefined,
409
+ ): boolean {
410
+ return (
411
+ typeof schema?.pattern === "string" && moneyPatterns.has(schema.pattern)
412
+ );
413
+ }
414
+
415
+ function referenceTargets(
416
+ schema: Readonly<Record<string, unknown>>,
417
+ nouns: readonly ParsedNoun[],
418
+ ): readonly ParsedNoun[] {
419
+ if (schema.type !== "string" || typeof schema.pattern !== "string") return [];
420
+ // Parse the scoped-ID pattern. Executing an author regex here would bypass
421
+ // the semantic validator's branch and quantifier budgets.
422
+ const match = /^\^([a-z]{2,8})_\(sandbox\|live\)_\[a-z0-9\]\{8,64\}\$$/.exec(
423
+ schema.pattern,
424
+ );
425
+ if (!match) return [];
426
+ const prefix = match[1] as string;
427
+ const probe = `${prefix}_sandbox_0123456789abcdef`;
428
+ if (typeof schema.const === "string" && schema.const !== probe) return [];
429
+ if (Array.isArray(schema.enum) && !schema.enum.includes(probe)) return [];
430
+ if (typeof schema.minLength === "number" && probe.length < schema.minLength) {
431
+ return [];
432
+ }
433
+ if (typeof schema.maxLength === "number" && probe.length > schema.maxLength) {
434
+ return [];
435
+ }
436
+ return nouns.filter((noun) => noun.idPrefix === prefix);
437
+ }
438
+
439
+ function declaredMoneyRefKeys(noun: ParsedNoun): ReadonlySet<string> {
440
+ return new Set([
441
+ ...(noun.computedMoneyRefs ?? []),
442
+ ...Object.values(noun.verbs).flatMap((verb) => [
443
+ ...(verb.signedSum
444
+ ? [
445
+ verb.signedSum.amountRef,
446
+ ...verb.signedSum.sources.map((source) => source.subtotalRef),
447
+ ]
448
+ : []),
449
+ ...(verb.distribute ? [verb.distribute.amountRef] : []),
450
+ ]),
451
+ ...(noun.unwind ? ["unwindRefund", "unwindPenalty"] : []),
452
+ ]);
453
+ }
454
+
455
+ export const udlDocumentSchema = udlDocumentShapeSchema.superRefine(
456
+ (document, context) => {
457
+ document.nouns.forEach((noun, nounIndex) => {
458
+ noun.derivedAmounts?.forEach((amount, amountIndex) => {
459
+ const base = [
460
+ "nouns",
461
+ nounIndex,
462
+ "derivedAmounts",
463
+ amountIndex,
464
+ ] as const;
465
+ if (!isMoneyField(noun.fields[amount.field])) {
466
+ context.addIssue({
467
+ code: "custom",
468
+ message: `derived amount target ${amount.field} must be a declared money field`,
469
+ path: [...base, "field"],
470
+ });
471
+ }
472
+ if (!isMoneyField(noun.fields[amount.sourceField])) {
473
+ context.addIssue({
474
+ code: "custom",
475
+ message: `derived amount source ${amount.sourceField} must be a declared money field`,
476
+ path: [...base, "sourceField"],
477
+ });
478
+ }
479
+ if (amount.field === amount.sourceField) {
480
+ context.addIssue({
481
+ code: "custom",
482
+ message: "a derived amount cannot derive from itself",
483
+ path: [...base, "sourceField"],
484
+ });
485
+ }
486
+ });
487
+
488
+ Object.entries(noun.verbs).forEach(([verbName, verb]) => {
489
+ const distribute = verb.distribute;
490
+ if (!distribute) return;
491
+ const base = [
492
+ "nouns",
493
+ nounIndex,
494
+ "verbs",
495
+ verbName,
496
+ "distribute",
497
+ ] as const;
498
+ const refSchema = noun.fields[distribute.refField];
499
+ const parents = refSchema
500
+ ? referenceTargets(refSchema, document.nouns)
501
+ : [];
502
+ if (parents.length !== 1) {
503
+ context.addIssue({
504
+ code: "custom",
505
+ message: `distribute refField ${distribute.refField} must identify exactly one parent noun`,
506
+ path: [...base, "refField"],
507
+ });
508
+ }
509
+
510
+ if (!isMoneyField(noun.fields[distribute.weightField])) {
511
+ context.addIssue({
512
+ code: "custom",
513
+ message: `distribute weightField ${distribute.weightField} must be a declared money field`,
514
+ path: [...base, "weightField"],
515
+ });
516
+ }
517
+
518
+ distribute.statuses.forEach((status, statusIndex) => {
519
+ if (noun.lifecycle.states.includes(status)) return;
520
+ context.addIssue({
521
+ code: "custom",
522
+ message: `distribute status ${status} is not declared by ${noun.id}`,
523
+ path: [...base, "statuses", statusIndex],
524
+ });
525
+ });
526
+
527
+ const parent = parents[0];
528
+ if (!parent) return;
529
+ const poolMatch = /^(fields|refs)\.([a-z][A-Za-z0-9]*)$/.exec(
530
+ distribute.pool.path,
531
+ );
532
+ const root = poolMatch?.[1];
533
+ const key = poolMatch?.[2];
534
+ const poolDeclared =
535
+ root === "fields"
536
+ ? isMoneyField(parent.fields[key ?? ""])
537
+ : root === "refs"
538
+ ? declaredMoneyRefKeys(parent).has(key ?? "")
539
+ : false;
540
+ if (!poolDeclared) {
541
+ context.addIssue({
542
+ code: "custom",
543
+ message: `distribute pool ${distribute.pool.path} must be a declared money field or ref of ${parent.id}`,
544
+ path: [...base, "pool", "path"],
545
+ });
546
+ }
547
+ });
548
+ });
549
+ },
550
+ );
551
+
317
552
  export type UdlAggregate = z.infer<typeof udlAggregateSchema>;
318
553
  export type UdlAggregateCondition = z.infer<typeof udlAggregateConditionSchema>;
319
554
  export type UdlBinding = z.infer<typeof udlBindingSchema>;
@@ -333,6 +568,8 @@ export type UdlLifecycleTransition = z.infer<
333
568
  export type UdlNoun = z.infer<typeof udlNounSchema>;
334
569
  export type UdlNounSubject = z.infer<typeof udlNounSubjectSchema>;
335
570
  export type UdlMove = z.infer<typeof udlMoveSchema>;
571
+ export type UdlPayout = z.infer<typeof udlPayoutSchema>;
572
+ export type UdlRequiresSettlement = z.infer<typeof udlRequiresSettlementSchema>;
336
573
  export type UdlStep = z.infer<typeof udlStepSchema>;
337
574
  export type UdlSubject = z.infer<typeof udlSubjectSchema>;
338
575
  export type UdlUnwind = z.infer<typeof udlUnwindSchema>;