@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/CHANGELOG.md +56 -1
- package/README.md +22 -20
- package/dist/evolution.d.ts +17 -0
- package/dist/evolution.d.ts.map +1 -1
- package/dist/evolution.js +54 -0
- package/dist/evolution.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +207 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +205 -1
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts +6 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +323 -14
- package/dist/validation.js.map +1 -1
- package/package.json +1 -1
- package/spec/README.md +10 -7
- package/spec/udl.schema.json +289 -0
- package/src/evolution.ts +99 -0
- package/src/index.ts +4 -0
- package/src/schema.ts +238 -1
- package/src/validation.ts +504 -19
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
|
-
|
|
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>;
|