@hyperscale0/udl 2.2.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 (88) hide show
  1. package/CHANGELOG.md +38 -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 +0 -6
  26. package/dist/effects.d.ts.map +1 -1
  27. package/dist/effects.js +84 -22
  28. package/dist/effects.js.map +1 -1
  29. package/dist/evolution.d.ts +12 -0
  30. package/dist/evolution.d.ts.map +1 -1
  31. package/dist/evolution.js +42 -1
  32. package/dist/evolution.js.map +1 -1
  33. package/dist/finance.d.ts +18 -1
  34. package/dist/finance.d.ts.map +1 -1
  35. package/dist/finance.js +158 -27
  36. package/dist/finance.js.map +1 -1
  37. package/dist/index.d.ts +9 -3
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +6 -2
  40. package/dist/index.js.map +1 -1
  41. package/dist/instrument-references.d.ts +5 -0
  42. package/dist/instrument-references.d.ts.map +1 -0
  43. package/dist/instrument-references.js +69 -0
  44. package/dist/instrument-references.js.map +1 -0
  45. package/dist/limits.d.ts +3 -3
  46. package/dist/limits.d.ts.map +1 -1
  47. package/dist/limits.js +3 -7
  48. package/dist/limits.js.map +1 -1
  49. package/dist/reference.d.ts +3 -0
  50. package/dist/reference.d.ts.map +1 -0
  51. package/dist/reference.js +28 -0
  52. package/dist/reference.js.map +1 -0
  53. package/dist/schema.d.ts +1232 -83
  54. package/dist/schema.d.ts.map +1 -1
  55. package/dist/schema.js +398 -60
  56. package/dist/schema.js.map +1 -1
  57. package/dist/validation.d.ts +27 -1
  58. package/dist/validation.d.ts.map +1 -1
  59. package/dist/validation.js +327 -187
  60. package/dist/validation.js.map +1 -1
  61. package/dist/vocabulary.d.ts +23 -0
  62. package/dist/vocabulary.d.ts.map +1 -0
  63. package/dist/vocabulary.js +965 -0
  64. package/dist/vocabulary.js.map +1 -0
  65. package/docs/README.md +5 -1
  66. package/docs/funding-custody.md +165 -0
  67. package/docs/guide/09-schedules-and-allocation.md +130 -0
  68. package/docs/llms-full.txt +623 -101
  69. package/docs/llms.txt +1 -1
  70. package/docs/piece-plans.md +148 -0
  71. package/docs/reference/clauses.md +451 -63
  72. package/docs/reference/cli.md +3 -1
  73. package/docs/reference/diagnostics.md +33 -38
  74. package/package.json +5 -6
  75. package/spec/udl.schema.json +1095 -118
  76. package/src/allocation.ts +259 -0
  77. package/src/diagnostics.ts +0 -32
  78. package/src/distribution.ts +61 -0
  79. package/src/effects.ts +116 -22
  80. package/src/evolution.ts +63 -3
  81. package/src/finance.ts +218 -24
  82. package/src/index.ts +31 -3
  83. package/src/instrument-references.ts +98 -0
  84. package/src/limits.ts +3 -7
  85. package/src/reference.ts +31 -0
  86. package/src/schema.ts +417 -66
  87. package/src/validation.ts +452 -243
  88. package/src/vocabulary.ts +1508 -0
package/src/finance.ts CHANGED
@@ -22,7 +22,24 @@ interface FinancialMove extends FinancialStep {
22
22
  readonly key: string;
23
23
  }
24
24
 
25
- interface FinancialInstrument {
25
+ export interface FinancialPartition {
26
+ readonly totalField: string;
27
+ readonly pieceFields: readonly string[];
28
+ }
29
+
30
+ export interface FinancialContribution {
31
+ readonly field: string;
32
+ readonly amountKey: string;
33
+ readonly accountKey: string;
34
+ readonly totalField: string;
35
+ }
36
+
37
+ export interface FinancialContributionStage {
38
+ readonly stage: "fund" | "refund";
39
+ readonly accountPath: string;
40
+ }
41
+
42
+ export interface FinancialInstrument {
26
43
  readonly lifecycle: {
27
44
  readonly initial: string;
28
45
  readonly states: readonly string[];
@@ -37,6 +54,8 @@ interface FinancialInstrument {
37
54
  readonly subjectHolder?: string | undefined;
38
55
  }
39
56
  | undefined;
57
+ readonly partitions?: readonly FinancialPartition[] | undefined;
58
+ readonly contributions?: FinancialContribution | undefined;
40
59
  readonly actions: Readonly<
41
60
  Record<
42
61
  string,
@@ -57,6 +76,7 @@ interface FinancialInstrument {
57
76
  }
58
77
  | undefined;
59
78
  readonly steps: readonly FinancialStep[];
79
+ readonly contributionStage?: FinancialContributionStage | undefined;
60
80
  }
61
81
  >
62
82
  >;
@@ -90,6 +110,7 @@ type Balance =
90
110
  interface AccountState {
91
111
  readonly balance: Balance;
92
112
  readonly holds: Readonly<Record<string, Balance>>;
113
+ readonly exchanged?: boolean | undefined;
93
114
  }
94
115
 
95
116
  interface Reservation {
@@ -348,13 +369,9 @@ function productEscrowAccounts(instrument: FinancialInstrument): Set<string> {
348
369
  const accounts = new Set<string>();
349
370
  for (const action of Object.values(instrument.actions)) {
350
371
  for (const step of action.steps) {
351
- if (
352
- step.operation !== "account.escrow.provision" ||
353
- step.bind.role?.from !== "const" ||
354
- step.bind.role.value !== "product_escrow"
355
- ) {
356
- continue;
357
- }
372
+ // This operation fixes the escrow role; an omitted redundant binding
373
+ // must not exempt its captured account from the conservation proof.
374
+ if (step.operation !== "account.escrow.provision") continue;
358
375
  for (const [key, result] of Object.entries(step.capture ?? {})) {
359
376
  if (result === "accountId") accounts.add(`ref:${key}`);
360
377
  }
@@ -366,8 +383,17 @@ function productEscrowAccounts(instrument: FinancialInstrument): Set<string> {
366
383
  // remain runtime balance checks. Commit sources are added separately above.
367
384
  return new Set(
368
385
  [...accounts].filter((account) =>
369
- Object.values(instrument.actions).some((action) =>
370
- (action.moves ?? []).some((step) => {
386
+ Object.values(instrument.actions).some((action) => {
387
+ if (
388
+ action.contributionStage &&
389
+ canonicalAccountPath(
390
+ instrument,
391
+ action.contributionStage.accountPath,
392
+ ) === account
393
+ ) {
394
+ return true;
395
+ }
396
+ return (action.moves ?? []).some((step) => {
371
397
  if (
372
398
  (step.operation !== "internal_transfer.create" &&
373
399
  step.operation !== "internal_transfer.reserve") ||
@@ -381,8 +407,8 @@ function productEscrowAccounts(instrument: FinancialInstrument): Set<string> {
381
407
  canonicalAccount(instrument, step.bind.destinationAccountId) ===
382
408
  account
383
409
  );
384
- }),
385
- ),
410
+ });
411
+ }),
386
412
  ),
387
413
  );
388
414
  }
@@ -425,9 +451,37 @@ function effectsByAction(
425
451
  quotes.map((quote) => [`refs.${quote.chargeRef}`, quote]),
426
452
  );
427
453
  return new Map(
428
- Object.entries(instrument.actions).map(([actionName, action]) => [
429
- actionName,
430
- (action.moves ?? []).flatMap((step, stepIndex) => {
454
+ Object.entries(instrument.actions).map(([actionName, action]) => {
455
+ const stageEffects: Effect[] = [];
456
+ if (action.contributionStage && instrument.contributions) {
457
+ const stage = action.contributionStage;
458
+ const stageAccount = canonicalAccountPath(
459
+ instrument,
460
+ stage.accountPath,
461
+ );
462
+ if (
463
+ stageAccount &&
464
+ accountsMayAlias(instrument, stageAccount, account)
465
+ ) {
466
+ const path = ["actions", actionName, "contributionStage"] as const;
467
+ const totalAmount = `fields.${instrument.contributions.totalField}`;
468
+ if (stage.stage === "fund") {
469
+ stageEffects.push({
470
+ amount: totalAmount,
471
+ kind: "credit",
472
+ path,
473
+ });
474
+ } else if (stage.stage === "refund") {
475
+ stageEffects.push({
476
+ amount: totalAmount,
477
+ kind: "debit",
478
+ path,
479
+ });
480
+ }
481
+ }
482
+ }
483
+
484
+ const moveEffects = (action.moves ?? []).flatMap((step, stepIndex) => {
431
485
  const path = [
432
486
  "actions",
433
487
  actionName,
@@ -537,11 +591,95 @@ function effectsByAction(
537
591
  ];
538
592
  }
539
593
  return [];
540
- }),
541
- ]),
594
+ });
595
+ return [actionName, [...stageEffects, ...moveEffects]];
596
+ }),
542
597
  );
543
598
  }
544
599
 
600
+ interface NormalizedPartition {
601
+ readonly total: string;
602
+ readonly pieces: readonly string[];
603
+ }
604
+
605
+ function getInstrumentPartitions(
606
+ instrument: FinancialInstrument,
607
+ ): readonly NormalizedPartition[] {
608
+ if (!instrument.partitions) return [];
609
+ const partitions: NormalizedPartition[] = [];
610
+ for (const p of instrument.partitions) {
611
+ if (!p.totalField || !p.pieceFields || p.pieceFields.length === 0) continue;
612
+ const total = `fields.${p.totalField}`;
613
+ const pieces = p.pieceFields.map((f) => `fields.${f}`);
614
+ if (new Set(pieces).size !== pieces.length) continue;
615
+ if (pieces.includes(total) && pieces.length > 1) continue;
616
+ partitions.push({ total, pieces });
617
+ }
618
+ return partitions;
619
+ }
620
+
621
+ function tryPartitionSubstitution(
622
+ instrument: FinancialInstrument,
623
+ balance: Balance,
624
+ holds: Readonly<Record<string, Balance>>,
625
+ exchanged: boolean,
626
+ targetAmount: string,
627
+ ): Balance | null {
628
+ if (exchanged) return null;
629
+ if (balance.kind !== "funded") return null;
630
+ if (Object.keys(holds).length > 0) return null;
631
+
632
+ const declaredPartitions = getInstrumentPartitions(instrument);
633
+ if (declaredPartitions.length === 0) return null;
634
+
635
+ for (const pOut of declaredPartitions) {
636
+ const total = pOut.total;
637
+
638
+ let candidateOutPieces: readonly string[] | null = null;
639
+ if (pOut.pieces.includes(targetAmount)) {
640
+ candidateOutPieces = pOut.pieces;
641
+ } else if (targetAmount === total) {
642
+ candidateOutPieces = [total];
643
+ }
644
+ if (!candidateOutPieces) continue;
645
+
646
+ if (candidateOutPieces.length === 0) continue;
647
+ if (new Set(candidateOutPieces).size !== candidateOutPieces.length)
648
+ continue;
649
+
650
+ const incomingCandidates: (readonly string[])[] = [];
651
+
652
+ if (balance.amounts.includes(total)) {
653
+ incomingCandidates.push([total]);
654
+ }
655
+
656
+ for (const pIn of declaredPartitions) {
657
+ if (pIn.total !== total) continue;
658
+ if (pIn.pieces.every((piece) => balance.amounts.includes(piece))) {
659
+ incomingCandidates.push(pIn.pieces);
660
+ }
661
+ }
662
+
663
+ for (const candidateInPieces of incomingCandidates) {
664
+ if (
665
+ candidateInPieces.length === candidateOutPieces.length &&
666
+ candidateInPieces.every((piece) => candidateOutPieces!.includes(piece))
667
+ ) {
668
+ continue;
669
+ }
670
+
671
+ if (
672
+ balance.amounts.length === candidateInPieces.length &&
673
+ candidateInPieces.every((piece) => balance.amounts.includes(piece))
674
+ ) {
675
+ return funded(candidateOutPieces);
676
+ }
677
+ }
678
+ }
679
+
680
+ return null;
681
+ }
682
+
545
683
  function applyEffects(
546
684
  instrument: FinancialInstrument,
547
685
  account: string,
@@ -554,6 +692,7 @@ function applyEffects(
554
692
  ): AccountState {
555
693
  let balance = input.balance;
556
694
  const holds = { ...input.holds };
695
+ let exchanged = input.exchanged ?? false;
557
696
  const accountLabel = formatAccount(account);
558
697
  for (const effect of effects) {
559
698
  const fail = (message: string): void =>
@@ -579,8 +718,24 @@ function applyEffects(
579
718
  }
580
719
  if (effect.kind === "incoming_reserve") continue;
581
720
  if (effect.kind === "outgoing_reserve") {
582
- const held =
721
+ let held =
583
722
  balance.kind === "funded" && balance.amounts.includes(effect.amount);
723
+ if (!held && balance.kind === "funded") {
724
+ const substituted = tryPartitionSubstitution(
725
+ instrument,
726
+ balance,
727
+ holds,
728
+ exchanged,
729
+ effect.amount,
730
+ );
731
+ if (substituted !== null) {
732
+ exchanged = true;
733
+ balance = substituted;
734
+ held =
735
+ balance.kind === "funded" &&
736
+ balance.amounts.includes(effect.amount);
737
+ }
738
+ }
584
739
  if (!held) {
585
740
  fail(
586
741
  `cannot reserve ${effect.amount} from ${accountLabel}; that exact balance is not guaranteed`,
@@ -676,12 +831,36 @@ function applyEffects(
676
831
  balance = balance.kind === "empty" ? EMPTY : { kind: "unknown" };
677
832
  continue;
678
833
  }
679
- if (balance.kind === "funded" && balance.amounts.includes(effect.amount)) {
680
- balance = funded(withoutPiece(balance.amounts, effect.amount));
681
- continue;
834
+ if (balance.kind === "funded") {
835
+ if (balance.amounts.includes(effect.amount)) {
836
+ balance = funded(withoutPiece(balance.amounts, effect.amount));
837
+ continue;
838
+ }
839
+ const substituted = tryPartitionSubstitution(
840
+ instrument,
841
+ balance,
842
+ holds,
843
+ exchanged,
844
+ effect.amount,
845
+ );
846
+ if (substituted !== null) {
847
+ exchanged = true;
848
+ balance = substituted;
849
+ if (
850
+ balance.kind === "funded" &&
851
+ balance.amounts.includes(effect.amount)
852
+ ) {
853
+ balance = funded(withoutPiece(balance.amounts, effect.amount));
854
+ continue;
855
+ }
856
+ }
682
857
  }
683
858
  if (balance.kind === "empty") {
684
- fail(`can debit unfunded ${accountLabel}`);
859
+ fail(
860
+ exchanged
861
+ ? `cannot prove ${effect.amount} is available after the substituted partition was spent from ${accountLabel}`
862
+ : `can debit unfunded ${accountLabel}`,
863
+ );
685
864
  } else if (balance.kind === "penalty") {
686
865
  fail(
687
866
  `can debit ${accountLabel} after its refund left only the quoted charge`,
@@ -693,7 +872,14 @@ function applyEffects(
693
872
  }
694
873
  balance = EMPTY;
695
874
  }
696
- return { balance, holds };
875
+ // A held account permits at most one equal-partition substitution per funded
876
+ // balance, preventing cycles, duplicate minting, and double spending. When an
877
+ // account is completely drained to empty with no active holds, the exchange
878
+ // record resets for any distinct subsequent funding.
879
+ if (balance.kind === "empty" && Object.keys(holds).length === 0) {
880
+ exchanged = false;
881
+ }
882
+ return { balance, holds, exchanged };
697
883
  }
698
884
 
699
885
  /**
@@ -873,7 +1059,14 @@ function canonicalAccount(
873
1059
  binding: UdlBinding | undefined,
874
1060
  ): string | undefined {
875
1061
  if (binding?.from !== "instance") return undefined;
876
- const [root, key, extra] = binding.path.split(".");
1062
+ return canonicalAccountPath(instrument, binding.path);
1063
+ }
1064
+
1065
+ function canonicalAccountPath(
1066
+ instrument: FinancialInstrument,
1067
+ path: string,
1068
+ ): string | undefined {
1069
+ const [root, key, extra] = path.split(".");
877
1070
  if (!key || extra !== undefined) return undefined;
878
1071
  if (root === "fields") return `field:${key}`;
879
1072
  if (root === "refs") return `ref:${key}`;
@@ -961,6 +1154,7 @@ function stateKey(state: AccountState): string {
961
1154
  holds: Object.fromEntries(
962
1155
  Object.entries(state.holds).sort(([a], [b]) => a.localeCompare(b)),
963
1156
  ),
1157
+ exchanged: state.exchanged ?? false,
964
1158
  });
965
1159
  }
966
1160
 
package/src/index.ts CHANGED
@@ -1,3 +1,7 @@
1
+ export {
2
+ mapUdlInstrumentReferences,
3
+ referencedUdlInstrumentIds,
4
+ } from "./instrument-references.js";
1
5
  export { canonicalizeUdl, parseUdl } from "./parser.js";
2
6
  export { canonicalDigest, serializeUdl } from "./canonical.js";
3
7
  export { issue, udlDiagnostic, udlDiagnostics } from "./diagnostics.js";
@@ -52,7 +56,6 @@ export {
52
56
  udlClauseVocabulary,
53
57
  udlDocumentSchema,
54
58
  udlInstrumentActionIdSchema,
55
- udlJourneySchema,
56
59
  udlKernelOperationSchema,
57
60
  udlProviderFamilyIdSchema,
58
61
  udlPublicActionSchema,
@@ -88,8 +91,6 @@ export type {
88
91
  UdlLifecycleTransition,
89
92
  UdlInstrument,
90
93
  UdlInstrumentSubject,
91
- UdlJourney,
92
- UdlJourneyStep,
93
94
  UdlPayout,
94
95
  UdlQuote,
95
96
  UdlProviderFamilyId,
@@ -109,6 +110,7 @@ export {
109
110
  reconcileExceptionChildProblems,
110
111
  openReferenceShapeBudget,
111
112
  UdlError,
113
+ instrumentFinanceIssues,
112
114
  validateUdl,
113
115
  validateUdlJsonSchema,
114
116
  validateUdlSchemaValue,
@@ -118,3 +120,29 @@ export type {
118
120
  UdlValidationOptions,
119
121
  UdlValidationResult,
120
122
  } from "./validation.js";
123
+
124
+ export {
125
+ deriveUdlAmount,
126
+ deriveRemainder,
127
+ matchesOrdered,
128
+ matchesDateOrder,
129
+ matchesSchedule,
130
+ isDueBefore,
131
+ planContributions,
132
+ } from "./vocabulary.js";
133
+ export {
134
+ planAllocation,
135
+ planAllocationRefund,
136
+ matchesAllocationConsumption,
137
+ } from "./allocation.js";
138
+ export type { UdlAllocation, UdlAllocate, UdlContributions } from "./schema.js";
139
+
140
+ export type {
141
+ AllocationInput,
142
+ AllocationSlice,
143
+ AllocationBalance,
144
+ AllocationPosting,
145
+ AllocationCancellation,
146
+ } from "./allocation.js";
147
+
148
+ export { distributeReceiptAmounts } from "./distribution.js";
@@ -0,0 +1,98 @@
1
+ import { z } from "zod";
2
+ import {
3
+ udlDocumentSchema,
4
+ type UdlDocument,
5
+ type UdlInstrument,
6
+ } from "./schema.js";
7
+
8
+ type Schema = Record<string, unknown>;
9
+ const object = (value: unknown): value is Schema =>
10
+ value !== null && typeof value === "object" && !Array.isArray(value);
11
+ let documentSchema: Schema | undefined;
12
+
13
+ /** Visit identities through schema annotations, never through user JSON keys. */
14
+ export function mapUdlInstrumentReferences(
15
+ document: UdlDocument,
16
+ map: (id: string) => string,
17
+ ): UdlDocument {
18
+ const root = (documentSchema ??= z.toJSONSchema(udlDocumentSchema));
19
+ const result = structuredClone(document);
20
+ const locations = new Map<
21
+ string,
22
+ { parent: Schema | unknown[]; key: string | number; kind: string }
23
+ >();
24
+ function visit(
25
+ schema: unknown,
26
+ value: unknown,
27
+ parent: Schema | unknown[],
28
+ key: string | number,
29
+ path: readonly (string | number)[],
30
+ ): void {
31
+ if (!object(schema)) return;
32
+ if (typeof schema.$ref === "string") {
33
+ if (!schema.$ref.startsWith("#/"))
34
+ throw new Error("UDL reference schema must be local");
35
+ let target: unknown = root;
36
+ for (const part of schema.$ref.slice(2).split("/")) {
37
+ target = object(target)
38
+ ? target[part.replaceAll("~1", "/").replaceAll("~0", "~")]
39
+ : undefined;
40
+ }
41
+ visit(target, value, parent, key, path);
42
+ }
43
+ const kind = schema["x-udl-reference"];
44
+ if (typeof kind === "string" && typeof value === "string")
45
+ locations.set(JSON.stringify(path), { parent, key, kind });
46
+ for (const union of [schema.anyOf, schema.oneOf, schema.allOf])
47
+ if (Array.isArray(union))
48
+ for (const branch of union) visit(branch, value, parent, key, path);
49
+ if (Array.isArray(value)) {
50
+ value.forEach((entry, index) =>
51
+ visit(schema.items, entry, value, index, [...path, index]),
52
+ );
53
+ } else if (object(value)) {
54
+ const properties = object(schema.properties) ? schema.properties : {};
55
+ for (const [name, entry] of Object.entries(value))
56
+ visit(
57
+ properties[name] ?? schema.additionalProperties,
58
+ entry,
59
+ value,
60
+ name,
61
+ [...path, name],
62
+ );
63
+ }
64
+ }
65
+ visit(root, result, [result], 0, []);
66
+ for (const { parent, key, kind } of locations.values()) {
67
+ const holder = parent as Record<string | number, unknown>;
68
+ const value = holder[key] as string;
69
+ if (kind === "instrument") holder[key] = map(value);
70
+ else if (kind === "instrument_event") {
71
+ const separator = value.indexOf(".");
72
+ holder[key] = map(value.slice(0, separator)) + value.slice(separator);
73
+ }
74
+ }
75
+ return result;
76
+ }
77
+
78
+ export function referencedUdlInstrumentIds(
79
+ instrument: UdlInstrument,
80
+ ): readonly string[] {
81
+ const found = new Set<string>();
82
+ mapUdlInstrumentReferences(
83
+ {
84
+ udl: 1,
85
+ version: 1,
86
+ product: "references",
87
+ title: "References",
88
+ subjects: [],
89
+ instruments: [instrument],
90
+ },
91
+ (id) => {
92
+ found.add(id);
93
+ return id;
94
+ },
95
+ );
96
+ found.delete(instrument.id);
97
+ return [...found];
98
+ }
package/src/limits.ts CHANGED
@@ -11,7 +11,6 @@ export const UDL_LIMITS = Object.freeze({
11
11
  maxActionLeaves: 256,
12
12
  maxDepth: 24,
13
13
  maxKeyLength: 128,
14
- // The 33-instrument catalog includes its executable authored journeys.
15
14
  maxNodes: 20_000,
16
15
  maxPatternLength: 320,
17
16
  /**
@@ -25,13 +24,10 @@ export const UDL_LIMITS = Object.freeze({
25
24
  maxPatternPaths: 4_096,
26
25
  /**
27
26
  * Reference-shape classifications one `ReferenceShapeBudget` may buy. Each
28
- * one compiles a JSON Schema validator, and a document controls both factors
29
- * of the instruments x gate-fields product that asks for them.
27
+ * one classifies canonical reference pattern text without compiling a validator.
28
+ * This safety net covers a catalogue plus hundreds of authored instruments.
30
29
  */
31
- maxSchemaProbes: 2_048,
32
- // The complete catalog now carries executable authored journeys beside the
33
- // instrument mechanics, so its bounded source and string budgets include
34
- // that contract-owned corpus.
30
+ maxSchemaProbes: 131_072,
35
31
  maxSourceBytes: 1_024 * 1_024,
36
32
  maxStringLength: 2_048,
37
33
  maxTotalStringLength: 512 * 1_024,
@@ -0,0 +1,31 @@
1
+ /** Read the sealed ID grammar without executing document-authored patterns. */
2
+ export function referencePatternPrefix(
3
+ schema: Readonly<Record<string, unknown>>,
4
+ ): string | undefined {
5
+ if (schema.type !== "string" || typeof schema.pattern !== "string")
6
+ return undefined;
7
+ const match = /^\^([a-z]{2,8})_\(sandbox\|live\)_\[a-z0-9\]\{8,64\}\$$/.exec(
8
+ schema.pattern,
9
+ );
10
+ const prefix = match?.[1];
11
+ if (!prefix) return undefined;
12
+ const min = typeof schema.minLength === "number" ? schema.minLength : 0;
13
+ const max =
14
+ typeof schema.maxLength === "number" ? schema.maxLength : Infinity;
15
+ if (min > prefix.length + 73 || max < prefix.length + 14 || min > max)
16
+ return undefined;
17
+ const validId = (value: unknown): boolean => {
18
+ if (typeof value !== "string") return false;
19
+ const id = /^([a-z]{2,8})_(sandbox|live)_([a-z0-9]{8,64})$/.exec(value);
20
+ return id?.[1] === prefix && value.length >= min && value.length <= max;
21
+ };
22
+ if (schema.const !== undefined && !validId(schema.const)) return undefined;
23
+ if (
24
+ Array.isArray(schema.enum) &&
25
+ (!schema.enum.length ||
26
+ !schema.enum.every(validId) ||
27
+ (schema.const !== undefined && !schema.enum.includes(schema.const)))
28
+ )
29
+ return undefined;
30
+ return prefix;
31
+ }