@hyperscale0/udl 2.3.0 → 2.4.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 (86) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +5 -1
  3. package/conformance/invalid/invalid-journeys.expected.json +3 -19
  4. package/conformance/invalid/invalid-journeys.udl +47 -49
  5. package/conformance/valid/attested.expected.json +6 -0
  6. package/conformance/valid/attested.udl +251 -0
  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 +0 -15
  11. package/conformance/valid/vocabulary.expected.json +6 -0
  12. package/conformance/valid/vocabulary.udl +1999 -0
  13. package/dist/allocation.d.ts +60 -0
  14. package/dist/allocation.d.ts.map +1 -0
  15. package/dist/allocation.js +177 -0
  16. package/dist/allocation.js.map +1 -0
  17. package/dist/diagnostics.d.ts +1 -31
  18. package/dist/diagnostics.d.ts.map +1 -1
  19. package/dist/diagnostics.js +0 -30
  20. package/dist/diagnostics.js.map +1 -1
  21. package/dist/distribution.d.ts +15 -0
  22. package/dist/distribution.d.ts.map +1 -0
  23. package/dist/distribution.js +49 -0
  24. package/dist/distribution.js.map +1 -0
  25. package/dist/effects.d.ts.map +1 -1
  26. package/dist/effects.js +23 -1
  27. package/dist/effects.js.map +1 -1
  28. package/dist/evolution.d.ts +12 -0
  29. package/dist/evolution.d.ts.map +1 -1
  30. package/dist/evolution.js +42 -1
  31. package/dist/evolution.js.map +1 -1
  32. package/dist/finance.d.ts +18 -1
  33. package/dist/finance.d.ts.map +1 -1
  34. package/dist/finance.js +158 -27
  35. package/dist/finance.js.map +1 -1
  36. package/dist/index.d.ts +8 -2
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +5 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/instrument-references.d.ts +5 -0
  41. package/dist/instrument-references.d.ts.map +1 -0
  42. package/dist/instrument-references.js +69 -0
  43. package/dist/instrument-references.js.map +1 -0
  44. package/dist/limits.d.ts +3 -3
  45. package/dist/limits.d.ts.map +1 -1
  46. package/dist/limits.js +3 -7
  47. package/dist/limits.js.map +1 -1
  48. package/dist/reference.d.ts +3 -0
  49. package/dist/reference.d.ts.map +1 -0
  50. package/dist/reference.js +28 -0
  51. package/dist/reference.js.map +1 -0
  52. package/dist/schema.d.ts +1232 -83
  53. package/dist/schema.d.ts.map +1 -1
  54. package/dist/schema.js +398 -60
  55. package/dist/schema.js.map +1 -1
  56. package/dist/validation.d.ts +14 -0
  57. package/dist/validation.d.ts.map +1 -1
  58. package/dist/validation.js +75 -131
  59. package/dist/validation.js.map +1 -1
  60. package/dist/vocabulary.d.ts +23 -0
  61. package/dist/vocabulary.d.ts.map +1 -0
  62. package/dist/vocabulary.js +965 -0
  63. package/dist/vocabulary.js.map +1 -0
  64. package/docs/README.md +2 -1
  65. package/docs/funding-custody.md +165 -0
  66. package/docs/guide/09-schedules-and-allocation.md +130 -0
  67. package/docs/llms-full.txt +620 -101
  68. package/docs/llms.txt +1 -1
  69. package/docs/reference/clauses.md +451 -63
  70. package/docs/reference/cli.md +3 -1
  71. package/docs/reference/diagnostics.md +33 -38
  72. package/package.json +5 -6
  73. package/spec/udl.schema.json +1095 -118
  74. package/src/allocation.ts +259 -0
  75. package/src/diagnostics.ts +0 -32
  76. package/src/distribution.ts +61 -0
  77. package/src/effects.ts +27 -1
  78. package/src/evolution.ts +63 -3
  79. package/src/finance.ts +218 -24
  80. package/src/index.ts +30 -3
  81. package/src/instrument-references.ts +98 -0
  82. package/src/limits.ts +3 -7
  83. package/src/reference.ts +31 -0
  84. package/src/schema.ts +417 -66
  85. package/src/validation.ts +112 -182
  86. package/src/vocabulary.ts +1508 -0
package/src/validation.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { referencePatternPrefix } from "./reference.js";
2
+ import { validateVocabulary } from "./vocabulary.js";
1
3
  import {
2
4
  Validator,
3
5
  type OutputUnit,
@@ -68,6 +70,12 @@ export interface UdlValidationOptions {
68
70
  readonly requireDecisionPartyBindings?: boolean;
69
71
  }
70
72
 
73
+ /**
74
+ * Validate a complete document. Clause references to instruments must resolve
75
+ * within this document, even when the reference field is optional. A catalogue
76
+ * slice must include its referenced instruments; an external catalogue cannot
77
+ * supply missing product laws. Reference fields use the sealed public ID grammar.
78
+ */
71
79
  export function validateUdl(
72
80
  value: unknown,
73
81
  options: UdlValidationOptions = {},
@@ -296,6 +304,26 @@ function semanticIssues(
296
304
  validateDocumentSchemas(document, add);
297
305
  if (issues.length > 0) return issues;
298
306
 
307
+ // Vocabulary laws must accumulate with the existing semantic diagnostics.
308
+ validateVocabulary(document.instruments, (path, message) =>
309
+ add(
310
+ path,
311
+ message,
312
+ path.some((part) =>
313
+ [
314
+ "attests",
315
+ "requiresInput",
316
+ "engineOwned",
317
+ "captureEngine",
318
+ "unique",
319
+ "port",
320
+ ].includes(String(part)),
321
+ )
322
+ ? "UDL5001"
323
+ : shapeIssueCode(path),
324
+ ),
325
+ );
326
+
299
327
  addDuplicateIssues(
300
328
  document.subjects.map((subject) => subject.kind),
301
329
  ["subjects"],
@@ -316,7 +344,6 @@ function semanticIssues(
316
344
  );
317
345
 
318
346
  validateCompositionDials(document, add);
319
- validateInstrumentJourneys(document, add);
320
347
 
321
348
  const subjects = new Map(
322
349
  document.subjects.map((subject) => [subject.kind, subject] as const),
@@ -416,162 +443,6 @@ function semanticIssues(
416
443
  return issues;
417
444
  }
418
445
 
419
- const scopedIdPattern =
420
- /^\^([a-z]{2,8})_\(sandbox\|live\)_\[a-z0-9\]\{8,64\}\$$/;
421
-
422
- function camel(value: string): string {
423
- return value.replace(/_([a-z0-9])/g, (_match, character: string) =>
424
- character.toUpperCase(),
425
- );
426
- }
427
-
428
- function journeyReferenceFields(
429
- instrument: UdlInstrument,
430
- actionName: string,
431
- ): ReadonlyMap<string, string> {
432
- const action = instrument.actions[actionName];
433
- if (!action) return new Map();
434
- const fields = new Map<string, string>();
435
- const addSchema = (name: string, schema: unknown) => {
436
- const pattern = recordValue(schema).pattern;
437
- if (typeof pattern !== "string") return;
438
- const prefix = scopedIdPattern.exec(pattern)?.[1];
439
- if (prefix) fields.set(name, prefix);
440
- };
441
- if (actionName === "create") {
442
- const derivedFields = new Set(
443
- (action.requiresRefs ?? []).flatMap((gate) =>
444
- Object.keys(gate.bind ?? {}),
445
- ),
446
- );
447
- for (const name of instrument.required) {
448
- if (!derivedFields.has(name)) addSchema(name, instrument.fields[name]);
449
- }
450
- } else {
451
- fields.set(`${camel(instrument.id)}Id`, instrument.idPrefix);
452
- }
453
- const input = recordValue(action.input);
454
- const required = Array.isArray(input.required)
455
- ? input.required.filter((name): name is string => typeof name === "string")
456
- : [];
457
- const properties = recordValue(input.properties);
458
- for (const name of required) addSchema(name, properties[name]);
459
- return fields;
460
- }
461
-
462
- /**
463
- * Validate the part of authored journeys that canonical UDL can prove alone.
464
- * A caller with the operation catalog validates root-operation examples and
465
- * cross-kind bindings. UDL owns local examples, lifecycle order, and local
466
- * reference completeness so raw UDL cannot bypass those laws.
467
- */
468
- function validateInstrumentJourneys(
469
- document: UdlDocument,
470
- add: AddIssue,
471
- ): void {
472
- const instruments = new Map(
473
- document.instruments.map(
474
- (instrument) => [instrument.id, instrument] as const,
475
- ),
476
- );
477
- for (const [instrumentIndex, owner] of document.instruments.entries()) {
478
- for (const [journeyIndex, journey] of (owner.journeys ?? []).entries()) {
479
- const base = [
480
- "instruments",
481
- instrumentIndex,
482
- "journeys",
483
- journeyIndex,
484
- ] as const;
485
- const seen = new Set<string>();
486
- const createdKindByStep = new Map<string, string>();
487
- const stateByStep = new Map<string, string>();
488
- for (const [stepIndex, step] of journey.steps.entries()) {
489
- const stepBase = [...base, "steps", stepIndex] as const;
490
- if (step.id) {
491
- if (seen.has(step.id)) {
492
- add(
493
- [...stepBase, "id"],
494
- `journey ${journey.id} declares step id ${step.id} more than once`,
495
- "journey_duplicate_step_id",
496
- );
497
- }
498
- }
499
- const [instrumentId, actionName] = step.operation.split(".");
500
- const instrument = instrumentId
501
- ? instruments.get(instrumentId)
502
- : undefined;
503
- const action =
504
- instrument && actionName ? instrument.actions[actionName] : undefined;
505
- if (!instrument) {
506
- if (step.id) seen.add(step.id);
507
- continue;
508
- }
509
- if (!action || !actionName) {
510
- add(
511
- [...stepBase, "operation"],
512
- `journey ${journey.id} names unknown operation ${step.operation}`,
513
- "journey_unknown_operation",
514
- );
515
- continue;
516
- }
517
- if (
518
- !action.examples?.some((example) => example.name === step.example)
519
- ) {
520
- add(
521
- [...stepBase, "example"],
522
- `journey ${journey.id} names unknown example ${step.example} on ${step.operation}`,
523
- "journey_unknown_example",
524
- );
525
- }
526
- for (const [field, prefix] of journeyReferenceFields(
527
- instrument,
528
- actionName,
529
- )) {
530
- const producer = step.bind[field];
531
- if (!producer || !seen.has(producer)) {
532
- add(
533
- [...stepBase, "bind", field],
534
- `journey ${journey.id} must bind ${step.operation}.${field} to an earlier step`,
535
- "journey_unbound_reference",
536
- );
537
- continue;
538
- }
539
- const producedPrefix = createdKindByStep.get(producer);
540
- if (producedPrefix && producedPrefix !== prefix) {
541
- add(
542
- [...stepBase, "bind", field],
543
- `journey ${journey.id} binds ${step.operation}.${field} to ${producer}, which creates ${producedPrefix} instead of ${prefix}`,
544
- "journey_unbound_reference",
545
- );
546
- }
547
- }
548
- if (actionName === "create") {
549
- if (step.id) {
550
- createdKindByStep.set(step.id, instrument.idPrefix);
551
- stateByStep.set(step.id, instrument.lifecycle.initial);
552
- seen.add(step.id);
553
- }
554
- continue;
555
- }
556
- const transition = instrument.lifecycle.transitions[actionName];
557
- if (!transition) continue;
558
- const ownProducer = step.bind[`${camel(instrument.id)}Id`];
559
- const state = ownProducer ? stateByStep.get(ownProducer) : undefined;
560
- if (state && !transition.from.includes(state)) {
561
- add(
562
- [...stepBase, "operation"],
563
- `journey ${journey.id} runs ${step.operation} from ${state}; allowed states are ${transition.from.join(", ")}`,
564
- "journey_invalid_transition",
565
- );
566
- } else if (ownProducer) {
567
- stateByStep.set(ownProducer, transition.to);
568
- }
569
- if (step.id) seen.add(step.id);
570
- }
571
- }
572
- }
573
- }
574
-
575
446
  function structuralBudgetIssue(value: unknown): UdlIssue | undefined {
576
447
  let discovered = 1;
577
448
  let nodes = 0;
@@ -2916,7 +2787,11 @@ function validateActionUpdates(
2916
2787
  add(fieldPath, `updated field ${field} is an aggregate cap`, "UDL5008");
2917
2788
  }
2918
2789
  });
2919
- if (definition.moves.length > 0) {
2790
+ if (
2791
+ definition.moves.length > 0 ||
2792
+ definition.allocate ||
2793
+ definition.contributionStage
2794
+ ) {
2920
2795
  add(
2921
2796
  [...actionBase, "updates"],
2922
2797
  "an action cannot update fields while moving money",
@@ -2995,6 +2870,33 @@ function validateRemainder(
2995
2870
  "UDL4001",
2996
2871
  );
2997
2872
  }
2873
+ addDuplicateIssues(
2874
+ remainder.subtractPaths ?? [],
2875
+ [...remainderBase, "subtractPaths"],
2876
+ "remainder operand",
2877
+ add,
2878
+ );
2879
+ for (const path of remainder.subtractPaths ?? []) {
2880
+ const [root, key] = path.split(".");
2881
+ const declared =
2882
+ root === "fields" &&
2883
+ key !== undefined &&
2884
+ isMoneySchema(instrument.fields[key] ?? {}) &&
2885
+ !instrument.update?.fields.includes(key) &&
2886
+ !Object.values(instrument.actions).some((action) =>
2887
+ action.updates?.includes(key),
2888
+ );
2889
+ if (
2890
+ !declared ||
2891
+ path === remainder.totalPath ||
2892
+ path === `refs.${remainder.amountRef}`
2893
+ )
2894
+ add(
2895
+ [...remainderBase, "subtractPaths"],
2896
+ `remainder subtraction ${path} must name distinct immutable money`,
2897
+ "UDL4001",
2898
+ );
2899
+ }
2998
2900
  const [totalRoot, totalKey] = remainder.totalPath.split(".");
2999
2901
  const totalDeclared =
3000
2902
  (totalRoot === "fields" &&
@@ -3189,10 +3091,24 @@ function validateActions(
3189
3091
  );
3190
3092
 
3191
3093
  if (definition.requiresRefs) {
3094
+ const boundFields = new Map<string, string>();
3095
+ for (const gate of definition.requiresRefs) {
3096
+ for (const [field, path] of Object.entries(gate.bind ?? {})) {
3097
+ const source = `${gate.field}:${path}`;
3098
+ const previous = boundFields.get(field);
3099
+ if (previous !== undefined && previous !== source)
3100
+ add(
3101
+ [...actionBase, "requiresRefs"],
3102
+ `reference gates bind ${field} from conflicting sources`,
3103
+ "UDL5001",
3104
+ );
3105
+ boundFields.set(field, source);
3106
+ }
3107
+ }
3192
3108
  addDuplicateIssues(
3193
- definition.requiresRefs.map((gate) => gate.field),
3109
+ definition.requiresRefs.map((gate) => JSON.stringify(sortObject(gate))),
3194
3110
  [...actionBase, "requiresRefs"],
3195
- "gate field",
3111
+ "gate",
3196
3112
  add,
3197
3113
  );
3198
3114
  definition.requiresRefs.forEach((gate, gateIndex) => {
@@ -3302,6 +3218,7 @@ function validateActions(
3302
3218
  ...Object.entries(instrument.actions).flatMap(
3303
3219
  ([candidateAction, candidate]) => [
3304
3220
  ...Object.keys(candidate.captureInput ?? {}),
3221
+ ...Object.keys(candidate.captureEngine ?? {}),
3305
3222
  ...[...candidate.steps, ...candidate.moves].flatMap((step) =>
3306
3223
  Object.keys(step.capture ?? {}),
3307
3224
  ),
@@ -3502,7 +3419,7 @@ function validateActions(
3502
3419
  );
3503
3420
  }
3504
3421
  if (
3505
- definition.due.offset &&
3422
+ typeof definition.due.offset === "string" &&
3506
3423
  fixedIsoDurationMs(definition.due.offset) === null
3507
3424
  ) {
3508
3425
  add(
@@ -3537,7 +3454,7 @@ function validateActions(
3537
3454
  );
3538
3455
  }
3539
3456
  if (
3540
- definition.deadline.offset &&
3457
+ typeof definition.deadline.offset === "string" &&
3541
3458
  fixedIsoDurationMs(definition.deadline.offset) === null
3542
3459
  ) {
3543
3460
  add(
@@ -4137,9 +4054,10 @@ function validatePayoutsAndSettlement(
4137
4054
  }
4138
4055
 
4139
4056
  const reservedRefs = new Set([
4140
- ...Object.values(instrument.actions).flatMap((action) =>
4141
- Object.keys(action.captureInput ?? {}),
4142
- ),
4057
+ ...Object.values(instrument.actions).flatMap((action) => [
4058
+ ...Object.keys(action.captureInput ?? {}),
4059
+ ...Object.keys(action.captureEngine ?? {}),
4060
+ ]),
4143
4061
  ...Object.values(instrument.actions).flatMap((action) =>
4144
4062
  [...action.steps, ...action.moves].flatMap((step) =>
4145
4063
  Object.keys(step.capture ?? {}),
@@ -4166,6 +4084,9 @@ function validatePayoutsAndSettlement(
4166
4084
  ]
4167
4085
  : [],
4168
4086
  ),
4087
+ ...Object.values(instrument.actions).flatMap((action) =>
4088
+ action.allocate ? [action.allocate.capture] : [],
4089
+ ),
4169
4090
  ...quoteRefKeys(instrument),
4170
4091
  ...(instrument.subject ? ["subject"] : []),
4171
4092
  ]);
@@ -4698,9 +4619,10 @@ function declaredRefKeys(instrument: UdlInstrument): ReadonlySet<string> {
4698
4619
  ...Object.values(instrument.actions).flatMap((action) =>
4699
4620
  (action.reconcile ?? []).map((reconcile) => reconcile.capture),
4700
4621
  ),
4701
- ...Object.values(instrument.actions).flatMap((action) =>
4702
- Object.keys(action.captureInput ?? {}),
4703
- ),
4622
+ ...Object.values(instrument.actions).flatMap((action) => [
4623
+ ...Object.keys(action.captureInput ?? {}),
4624
+ ...Object.keys(action.captureEngine ?? {}),
4625
+ ]),
4704
4626
  ...Object.values(instrument.actions).flatMap((action) =>
4705
4627
  [...action.steps, ...action.moves].flatMap((step) =>
4706
4628
  Object.keys(step.capture ?? {}),
@@ -4714,6 +4636,9 @@ function declaredRefKeys(instrument: UdlInstrument): ReadonlySet<string> {
4714
4636
  ]
4715
4637
  : [],
4716
4638
  ),
4639
+ ...Object.values(instrument.actions).flatMap((action) =>
4640
+ action.allocate ? [action.allocate.capture] : [],
4641
+ ),
4717
4642
  ...quoteRefKeys(instrument),
4718
4643
  ...(instrument.subject ? ["subject"] : []),
4719
4644
  ]);
@@ -5554,34 +5479,28 @@ function reconcileExceptionProblemMessage(
5554
5479
  * are written once. The value is disposable: no document ever carries it, and
5555
5480
  * a host's real id grammar stays the host's business.
5556
5481
  */
5557
- const probeIdFor = (prefix: string): string =>
5558
- `${prefix}_sandbox_0123456789abcdef`;
5559
-
5560
- /** A prefix no host mints, so a schema that accepts it accepts anything. */
5561
- const UNCLAIMED_PREFIX = "zzzz";
5562
-
5482
+ /** Classify the sealed ID pattern without compiling document-authored regexes. */
5563
5483
  export function openReferenceShapeBudget(): ReferenceShapeBudget {
5564
5484
  const answers = new WeakMap<object, Map<string, boolean>>();
5565
5485
  let probes = 0;
5486
+ let exhausted = false;
5566
5487
  return {
5567
5488
  accepts(schema, prefix) {
5568
5489
  const seen = answers.get(schema);
5569
5490
  const cached = seen?.get(prefix);
5570
5491
  if (cached !== undefined) return cached;
5571
- if (probes >= UDL_LIMITS.maxSchemaProbes) return false;
5492
+ if (probes >= UDL_LIMITS.maxSchemaProbes) {
5493
+ exhausted = true;
5494
+ return false;
5495
+ }
5572
5496
  probes += 1;
5573
-
5574
- const answer =
5575
- validateUdlSchemaValue(schema, probeIdFor(prefix)).errors.length ===
5576
- 0 &&
5577
- validateUdlSchemaValue(schema, probeIdFor(UNCLAIMED_PREFIX)).errors
5578
- .length > 0;
5497
+ const answer = referencePatternPrefix(schema) === prefix;
5579
5498
  if (seen) seen.set(prefix, answer);
5580
5499
  else answers.set(schema, new Map([[prefix, answer]]));
5581
5500
  return answer;
5582
5501
  },
5583
5502
  get exhausted() {
5584
- return probes >= UDL_LIMITS.maxSchemaProbes;
5503
+ return exhausted;
5585
5504
  },
5586
5505
  };
5587
5506
  }
@@ -5726,3 +5645,14 @@ function jsonPath(path: readonly PropertyKey[]): string {
5726
5645
  }
5727
5646
  return result;
5728
5647
  }
5648
+
5649
+ function sortObject(value: unknown): unknown {
5650
+ if (Array.isArray(value)) return value.map(sortObject);
5651
+ if (value !== null && typeof value === "object")
5652
+ return Object.fromEntries(
5653
+ Object.entries(value)
5654
+ .sort(([a], [b]) => a.localeCompare(b))
5655
+ .map(([key, item]) => [key, sortObject(item)]),
5656
+ );
5657
+ return value;
5658
+ }