@hyperscale0/udl 1.0.0 → 2.0.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +19 -4
  2. package/README.md +24 -36
  3. package/conformance/invalid/invalid-journeys.expected.json +26 -0
  4. package/conformance/invalid/invalid-journeys.udl +79 -0
  5. package/conformance/valid/commerce-escrow.expected.json +1 -1
  6. package/conformance/valid/commerce-escrow.udl +1 -1
  7. package/conformance/valid/hand-edited.expected.json +1 -1
  8. package/conformance/valid/hand-edited.udl +1 -1
  9. package/conformance/valid/minimal.expected.json +1 -1
  10. package/conformance/valid/minimal.udl +23 -0
  11. package/dist/check-profiles.d.ts.map +1 -1
  12. package/dist/diagnostics.d.ts +31 -1
  13. package/dist/diagnostics.d.ts.map +1 -1
  14. package/dist/diagnostics.js +30 -0
  15. package/dist/diagnostics.js.map +1 -1
  16. package/dist/effects.d.ts.map +1 -1
  17. package/dist/evolution.js +30 -9
  18. package/dist/evolution.js.map +1 -1
  19. package/dist/finance.d.ts.map +1 -1
  20. package/dist/finance.js +37 -18
  21. package/dist/finance.js.map +1 -1
  22. package/dist/index.d.ts +3 -3
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +1 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/limits.d.ts +1 -1
  27. package/dist/limits.d.ts.map +1 -1
  28. package/dist/limits.js +7 -4
  29. package/dist/limits.js.map +1 -1
  30. package/dist/schema.d.ts +258 -147
  31. package/dist/schema.d.ts.map +1 -1
  32. package/dist/schema.js +33 -10
  33. package/dist/schema.js.map +1 -1
  34. package/dist/validation.d.ts +5 -1
  35. package/dist/validation.d.ts.map +1 -1
  36. package/dist/validation.js +176 -7
  37. package/dist/validation.js.map +1 -1
  38. package/docs/assets/udl.svg +18 -0
  39. package/docs/guide/03-laws.md +1 -1
  40. package/docs/guide/08-implementing.md +1 -1
  41. package/docs/llms-full.txt +62 -29
  42. package/docs/llms.txt +1 -1
  43. package/docs/reference/clauses.md +29 -1
  44. package/docs/reference/cli.md +1 -1
  45. package/docs/reference/diagnostics.md +32 -27
  46. package/package.json +6 -5
  47. package/spec/README.md +8 -16
  48. package/spec/udl.schema.json +83 -15
  49. package/src/diagnostics.ts +32 -0
  50. package/src/evolution.ts +31 -13
  51. package/src/finance.ts +53 -20
  52. package/src/index.ts +6 -0
  53. package/src/limits.ts +7 -4
  54. package/src/schema.ts +48 -10
  55. package/src/validation.ts +252 -6
package/src/evolution.ts CHANGED
@@ -157,7 +157,7 @@ export function snapshotUdlInstrument(
157
157
  fields: Object.fromEntries(
158
158
  Object.entries(instrument.fields).map(([field, schema]) => [
159
159
  field,
160
- { required: required.has(field), schema },
160
+ { required: required.has(field), schema: withoutProse(schema) },
161
161
  ]),
162
162
  ),
163
163
  id: instrument.id,
@@ -575,11 +575,34 @@ function snapshotJsonSchemaFields(
575
575
  return Object.fromEntries(
576
576
  Object.entries(properties).map(([field, fieldSchema]) => [
577
577
  field,
578
- { required: required.has(field), schema: fieldSchema },
578
+ { required: required.has(field), schema: withoutProse(fieldSchema) },
579
579
  ]),
580
580
  );
581
581
  }
582
582
 
583
+ /**
584
+ * Descriptions are prose for readers, not shape: a wording fix in a std
585
+ * template must land on a live flow as an extension, never as a refusal.
586
+ */
587
+ function withoutProse(value: unknown, depth = 1): unknown {
588
+ if (depth > UDL_LIMITS.maxDepth) throw nestingExceeded();
589
+ if (Array.isArray(value)) {
590
+ return value.map((item) => withoutProse(item, depth + 1));
591
+ }
592
+ if (value === null || typeof value !== "object") return value;
593
+ return Object.fromEntries(
594
+ Object.entries(value as Record<string, unknown>)
595
+ .filter(([key]) => key !== "description")
596
+ .map(([key, entry]) => [key, withoutProse(entry, depth + 1)]),
597
+ );
598
+ }
599
+
600
+ function nestingExceeded(): UdlError {
601
+ return new UdlError([
602
+ issue("UDL1004", "$", `UDL nesting exceeds ${UDL_LIMITS.maxDepth} levels`),
603
+ ]);
604
+ }
605
+
583
606
  function snapshotJsonSchemaConstraints(
584
607
  schema: Readonly<Record<string, unknown>>,
585
608
  ): Readonly<Record<string, unknown>> {
@@ -753,9 +776,12 @@ function diffActions(
753
776
  ) {
754
777
  violations.push(`action ${action} changed its check prerequisites`);
755
778
  }
779
+ // A failure point only adds a sandbox refusal for one documented amount, so
780
+ // declaring one on an action that had none is additive; changing or
781
+ // removing a declared one rewrites what a sandbox integration relies on.
756
782
  if (
757
- (descriptor.sandboxFailurePoint ?? null) !==
758
- (current.sandboxFailurePoint ?? null)
783
+ descriptor.sandboxFailurePoint &&
784
+ descriptor.sandboxFailurePoint !== (current.sandboxFailurePoint ?? null)
759
785
  ) {
760
786
  violations.push(`action ${action} changed its sandbox failure point`);
761
787
  }
@@ -937,15 +963,7 @@ function recordValue(value: unknown): Readonly<Record<string, unknown>> {
937
963
  * and lands here rather than exhausting the call stack.
938
964
  */
939
965
  function stableStringify(value: unknown, depth = 1): string {
940
- if (depth > UDL_LIMITS.maxDepth) {
941
- throw new UdlError([
942
- issue(
943
- "UDL1004",
944
- "$",
945
- `UDL nesting exceeds ${UDL_LIMITS.maxDepth} levels`,
946
- ),
947
- ]);
948
- }
966
+ if (depth > UDL_LIMITS.maxDepth) throw nestingExceeded();
949
967
  if (value === null || typeof value !== "object") {
950
968
  return JSON.stringify(value) ?? "null";
951
969
  }
package/src/finance.ts CHANGED
@@ -207,6 +207,7 @@ export function analyzeInstrumentFinance(
207
207
  return issues;
208
208
  }
209
209
  const initial = applyEffects(
210
+ instrument,
210
211
  account,
211
212
  "create",
212
213
  { balance: EMPTY, holds: {} },
@@ -243,6 +244,7 @@ export function analyzeInstrumentFinance(
243
244
  return issues;
244
245
  }
245
246
  const targetState = applyEffects(
247
+ instrument,
246
248
  account,
247
249
  action,
248
250
  sourceState,
@@ -274,19 +276,21 @@ export function analyzeInstrumentFinance(
274
276
  (transition) => transition.from,
275
277
  ),
276
278
  );
277
- for (const [state, variants] of states) {
278
- if (nonterminalStates.has(state)) continue;
279
- for (const variant of variants.values()) {
280
- if (
281
- variant.balance.kind === "empty" &&
282
- Object.keys(variant.holds).length === 0
283
- ) {
284
- continue;
279
+ if (account.startsWith("ref:")) {
280
+ for (const [state, variants] of states) {
281
+ if (nonterminalStates.has(state)) continue;
282
+ for (const variant of variants.values()) {
283
+ if (
284
+ variant.balance.kind === "empty" &&
285
+ Object.keys(variant.holds).length === 0
286
+ ) {
287
+ continue;
288
+ }
289
+ add(
290
+ ["lifecycle", "states", instrument.lifecycle.states.indexOf(state)],
291
+ `terminal state ${state} can strand value in ${formatAccount(account)}`,
292
+ );
285
293
  }
286
- add(
287
- ["lifecycle", "states", instrument.lifecycle.states.indexOf(state)],
288
- `terminal state ${state} can strand value in ${formatAccount(account)}`,
289
- );
290
294
  }
291
295
  }
292
296
  }
@@ -434,7 +438,7 @@ function effectsByAction(
434
438
  instrument,
435
439
  step.bind.sourceAccountId,
436
440
  );
437
- if (source && accountsMayAlias(source, account)) {
441
+ if (source && accountsMayAlias(instrument, source, account)) {
438
442
  const committed = quoteByCommit.get(actionName);
439
443
  const amountPath =
440
444
  step.bind.amount?.from === "instance"
@@ -474,7 +478,7 @@ function effectsByAction(
474
478
  instrument,
475
479
  step.bind.sourceAccountId,
476
480
  );
477
- if (source && accountsMayAlias(source, account)) {
481
+ if (source && accountsMayAlias(instrument, source, account)) {
478
482
  effects.push({
479
483
  amount: amountIdentity(step.bind.amount),
480
484
  kind: "outgoing_reserve",
@@ -504,7 +508,10 @@ function effectsByAction(
504
508
  const reserved = reservations.get(reservation);
505
509
  if (
506
510
  !reserved ||
507
- (!(reserved.source && accountsMayAlias(reserved.source, account)) &&
511
+ (!(
512
+ reserved.source &&
513
+ accountsMayAlias(instrument, reserved.source, account)
514
+ ) &&
508
515
  reserved.destination !== account)
509
516
  ) {
510
517
  return [];
@@ -526,6 +533,7 @@ function effectsByAction(
526
533
  }
527
534
 
528
535
  function applyEffects(
536
+ instrument: FinancialInstrument,
529
537
  account: string,
530
538
  action: string,
531
539
  input: AccountState,
@@ -582,7 +590,7 @@ function applyEffects(
582
590
  const reservation = reservations.get(key);
583
591
  if (
584
592
  reservation?.source &&
585
- accountsMayAlias(reservation.source, account)
593
+ accountsMayAlias(instrument, reservation.source, account)
586
594
  ) {
587
595
  const held = holds[key];
588
596
  delete holds[key];
@@ -784,7 +792,7 @@ function validateChargePayout(
784
792
  source !== undefined &&
785
793
  source === refundSource &&
786
794
  destination !== undefined &&
787
- !accountsMayAlias(source, destination);
795
+ !accountsMayAlias(instrument, source, destination);
788
796
  if (!valid) {
789
797
  add(
790
798
  ["actions", use.actionName, "moves", use.stepIndex, "bind", use.target],
@@ -812,9 +820,34 @@ function canonicalAccount(
812
820
  return undefined;
813
821
  }
814
822
 
815
- function accountsMayAlias(left: string, right: string): boolean {
816
- if (left.startsWith("field:") && right.startsWith("field:")) return true;
817
- return left === right;
823
+ function accountsMayAlias(
824
+ instrument: FinancialInstrument,
825
+ left: string,
826
+ right: string,
827
+ ): boolean {
828
+ if (left === right) return true;
829
+ if (left.startsWith("field:") && right.startsWith("field:")) {
830
+ const leftField = left.slice("field:".length);
831
+ const rightField = right.slice("field:".length);
832
+ if (leftField === rightField) return true;
833
+ if (instrument.parties) {
834
+ const leftParty = Object.entries(instrument.parties).find(
835
+ ([, field]) => field === leftField,
836
+ )?.[0];
837
+ const rightParty = Object.entries(instrument.parties).find(
838
+ ([, field]) => field === rightField,
839
+ )?.[0];
840
+ if (
841
+ leftParty !== undefined &&
842
+ rightParty !== undefined &&
843
+ leftParty !== rightParty
844
+ ) {
845
+ return false;
846
+ }
847
+ }
848
+ return true;
849
+ }
850
+ return false;
818
851
  }
819
852
 
820
853
  function amountIdentity(binding: UdlBinding | undefined): string {
package/src/index.ts CHANGED
@@ -48,6 +48,7 @@ export {
48
48
  udlClauseVocabulary,
49
49
  udlDocumentSchema,
50
50
  udlInstrumentActionIdSchema,
51
+ udlJourneySchema,
51
52
  udlKernelOperationSchema,
52
53
  udlProviderFamilyIdSchema,
53
54
  udlPublicActionSchema,
@@ -64,12 +65,16 @@ export type {
64
65
  UdlDue,
65
66
  UdlEffects,
66
67
  UdlExample,
68
+ UdlDateComparison,
69
+ UdlDateComparisonOperator,
67
70
  UdlGate,
68
71
  UdlKernelOperation,
69
72
  UdlLifecycle,
70
73
  UdlLifecycleTransition,
71
74
  UdlInstrument,
72
75
  UdlInstrumentSubject,
76
+ UdlJourney,
77
+ UdlJourneyStep,
73
78
  UdlPayout,
74
79
  UdlQuote,
75
80
  UdlProviderFamilyId,
@@ -95,5 +100,6 @@ export {
95
100
  } from "./validation.js";
96
101
  export type {
97
102
  ReferenceShapeBudget,
103
+ UdlValidationOptions,
98
104
  UdlValidationResult,
99
105
  } from "./validation.js";
package/src/limits.ts CHANGED
@@ -9,7 +9,8 @@ export const UDL_LIMITS = Object.freeze({
9
9
  financeWork: 4_096,
10
10
  maxDepth: 24,
11
11
  maxKeyLength: 128,
12
- maxNodes: 10_000,
12
+ // The 33-instrument catalog includes its executable authored journeys.
13
+ maxNodes: 20_000,
13
14
  maxPatternLength: 320,
14
15
  /**
15
16
  * Upper bound on the match attempts a document-authored `pattern` can force
@@ -26,8 +27,10 @@ export const UDL_LIMITS = Object.freeze({
26
27
  * of the instruments x gate-fields product that asks for them.
27
28
  */
28
29
  maxSchemaProbes: 2_048,
29
- // The complete 2026-09-01 platform catalog serializes to 402,126 bytes.
30
- maxSourceBytes: 512 * 1_024,
30
+ // The complete catalog now carries executable authored journeys beside the
31
+ // instrument mechanics, so its bounded source and string budgets include
32
+ // that contract-owned corpus.
33
+ maxSourceBytes: 1_024 * 1_024,
31
34
  maxStringLength: 2_048,
32
- maxTotalStringLength: 256 * 1_024,
35
+ maxTotalStringLength: 512 * 1_024,
33
36
  });
package/src/schema.ts CHANGED
@@ -33,6 +33,14 @@ export const udlInstrumentActionIdSchema = z
33
33
  "must name an instrument action as instrument_id.action_key",
34
34
  );
35
35
 
36
+ const udlJourneyOperationNameSchema = z
37
+ .string()
38
+ .max(160)
39
+ .regex(
40
+ /^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$/,
41
+ "must name a dotted operation",
42
+ );
43
+
36
44
  const nonEmptyTextSchema = z
37
45
  .string()
38
46
  .refine((value) => value.trim().length > 0, "must not be blank");
@@ -93,6 +101,20 @@ const udlExampleSchema = z.strictObject({
93
101
  output: z.json().optional(),
94
102
  });
95
103
 
104
+ const udlJourneyStepSchema = z.strictObject({
105
+ bind: z.record(fieldPathSchema, udlSnakeCaseSchema),
106
+ example: udlSnakeCaseSchema,
107
+ id: udlSnakeCaseSchema.optional(),
108
+ operation: udlJourneyOperationNameSchema,
109
+ });
110
+
111
+ export const udlJourneySchema = z.strictObject({
112
+ id: udlSnakeCaseSchema,
113
+ label: nonEmptyTextSchema,
114
+ steps: z.array(udlJourneyStepSchema).min(1),
115
+ summary: nonEmptyTextSchema,
116
+ });
117
+
96
118
  const udlLifecycleTransitionSchema = z.strictObject({
97
119
  from: z.array(udlSnakeCaseSchema).min(1),
98
120
  to: udlSnakeCaseSchema,
@@ -110,9 +132,18 @@ const udlUpdatePolicySchema = z.strictObject({
110
132
  states: z.array(udlSnakeCaseSchema).min(1),
111
133
  });
112
134
 
135
+ const udlDateComparisonOperatorSchema = z.enum([">=", ">", "<=", "<", "=="]);
136
+
137
+ const udlDateComparisonSchema = z.strictObject({
138
+ localPath: fieldPathSchema,
139
+ operator: udlDateComparisonOperatorSchema,
140
+ referencedPath: fieldPathSchema,
141
+ });
142
+
113
143
  const udlGateSchema = z.strictObject({
114
144
  /** Local field key <- referenced instance path; create actions only. */
115
145
  bind: z.record(udlFieldNameSchema, fieldPathSchema).optional(),
146
+ dateComparison: udlDateComparisonSchema.optional(),
116
147
  field: udlFieldNameSchema,
117
148
  /** Local instance path === referenced instance path at admission. */
118
149
  match: z.record(fieldPathSchema, fieldPathSchema).optional(),
@@ -329,10 +360,10 @@ const udlExposureRequirementSchema = z.strictObject({
329
360
  // The tenant-backend decision port: the action's caller asserts the acting
330
361
  // party, checked at admission against the instrument's party bindings for the
331
362
  // allowed roles.
363
+ const udlPartyRoleSchema = z.string().regex(/^[a-z][A-Za-z0-9_]*$/);
364
+
332
365
  const udlPortSchema = z.strictObject({
333
- allowedParties: z
334
- .array(z.enum(["payer", "beneficiary", "subjectHolder"]))
335
- .min(1),
366
+ allowedParties: z.array(udlPartyRoleSchema).min(1),
336
367
  });
337
368
 
338
369
  const udlPayoutSchema = z.strictObject({
@@ -686,14 +717,9 @@ const udlInstrumentShape = {
686
717
  .string()
687
718
  .regex(idPrefixPattern, "must contain 2 to 8 lowercase letters"),
688
719
  lifecycle: udlLifecycleSchema,
720
+ journeys: z.array(udlJourneySchema).min(1).optional(),
689
721
  nav: z.array(nonEmptyTextSchema).min(1).optional(),
690
- parties: z
691
- .strictObject({
692
- beneficiary: udlFieldNameSchema.optional(),
693
- payer: udlFieldNameSchema.optional(),
694
- subjectHolder: udlFieldNameSchema.optional(),
695
- })
696
- .optional(),
722
+ parties: z.record(udlPartyRoleSchema, udlFieldNameSchema).optional(),
697
723
  partitions: z.array(udlPartitionSchema).min(1).optional(),
698
724
  required: z.array(udlFieldNameSchema),
699
725
  subject: udlInstrumentSubjectSchema.optional(),
@@ -1099,6 +1125,12 @@ export const udlClauseVocabulary = [
1099
1125
  spelling: "id prefix",
1100
1126
  target: "idPrefix",
1101
1127
  },
1128
+ {
1129
+ cardinality: "many",
1130
+ scope: "instrument",
1131
+ spelling: "journeys",
1132
+ target: "journeys",
1133
+ },
1102
1134
  {
1103
1135
  cardinality: "many",
1104
1136
  scope: "instrument",
@@ -1330,6 +1362,10 @@ export type UdlDocument = z.infer<typeof udlDocumentSchema>;
1330
1362
  export type UdlDue = z.infer<typeof udlDueSchema>;
1331
1363
  export type UdlEffects = z.infer<typeof udlEffectsSchema>;
1332
1364
  export type UdlExample = z.infer<typeof udlExampleSchema>;
1365
+ export type UdlDateComparison = z.infer<typeof udlDateComparisonSchema>;
1366
+ export type UdlDateComparisonOperator = z.infer<
1367
+ typeof udlDateComparisonOperatorSchema
1368
+ >;
1333
1369
  export type UdlGate = z.infer<typeof udlGateSchema>;
1334
1370
  export type UdlKernelOperation = z.infer<typeof udlKernelOperationSchema>;
1335
1371
  export type UdlLifecycle = z.infer<typeof udlLifecycleSchema>;
@@ -1338,6 +1374,8 @@ export type UdlLifecycleTransition = z.infer<
1338
1374
  >;
1339
1375
  export type UdlInstrument = z.infer<typeof udlInstrumentSchema>;
1340
1376
  export type UdlInstrumentSubject = z.infer<typeof udlInstrumentSubjectSchema>;
1377
+ export type UdlJourney = z.infer<typeof udlJourneySchema>;
1378
+ export type UdlJourneyStep = z.infer<typeof udlJourneyStepSchema>;
1341
1379
  export type UdlMove = z.infer<typeof udlMoveSchema>;
1342
1380
  export type UdlPayout = z.infer<typeof udlPayoutSchema>;
1343
1381
  export type UdlQuote = z.infer<typeof udlQuoteSchema>;