@hyperscale0/udl 2.6.0 → 3.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 (213) hide show
  1. package/CHANGELOG.md +2 -322
  2. package/LICENSING.md +1 -2
  3. package/README.md +3 -114
  4. package/TRADEMARKS.md +2 -2
  5. package/dist/diagnostics.d.ts +21 -201
  6. package/dist/diagnostics.d.ts.map +1 -1
  7. package/dist/diagnostics.js +29 -193
  8. package/dist/diagnostics.js.map +1 -1
  9. package/dist/evolution.d.ts +3 -132
  10. package/dist/evolution.d.ts.map +1 -1
  11. package/dist/evolution.js +29 -633
  12. package/dist/evolution.js.map +1 -1
  13. package/dist/finance.d.ts +5 -72
  14. package/dist/finance.d.ts.map +1 -1
  15. package/dist/finance.js +233 -737
  16. package/dist/finance.js.map +1 -1
  17. package/dist/index.d.ts +7 -18
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +7 -12
  20. package/dist/index.js.map +1 -1
  21. package/dist/instrument-references.d.ts.map +1 -1
  22. package/dist/instrument-references.js +3 -2
  23. package/dist/instrument-references.js.map +1 -1
  24. package/dist/schema.d.ts +2079 -4564
  25. package/dist/schema.d.ts.map +1 -1
  26. package/dist/schema.js +270 -1609
  27. package/dist/schema.js.map +1 -1
  28. package/dist/validation.d.ts +8 -104
  29. package/dist/validation.d.ts.map +1 -1
  30. package/dist/validation.js +653 -3271
  31. package/dist/validation.js.map +1 -1
  32. package/docs/README.md +147 -14
  33. package/package.json +7 -11
  34. package/spec/README.md +151 -164
  35. package/spec/darb.udl.json +249 -0
  36. package/spec/udl.schema.json +4104 -3202
  37. package/src/diagnostics.ts +51 -246
  38. package/src/evolution.ts +39 -1045
  39. package/src/finance.ts +287 -1127
  40. package/src/index.ts +18 -142
  41. package/src/instrument-references.ts +3 -2
  42. package/src/schema.ts +285 -1843
  43. package/src/validation.ts +870 -5611
  44. package/conformance/README.md +0 -82
  45. package/conformance/evolution/action-contract.expected.json +0 -10
  46. package/conformance/evolution/action-contract.live.udl +0 -44
  47. package/conformance/evolution/action-contract.next.udl +0 -45
  48. package/conformance/evolution/product-identity.expected.json +0 -10
  49. package/conformance/evolution/product-identity.live.udl +0 -55
  50. package/conformance/evolution/product-identity.next.udl +0 -55
  51. package/conformance/evolution/version-required.expected.json +0 -10
  52. package/conformance/evolution/version-required.live.udl +0 -55
  53. package/conformance/evolution/version-required.next.udl +0 -58
  54. package/conformance/invalid/action-without-transition.expected.json +0 -10
  55. package/conformance/invalid/action-without-transition.udl +0 -60
  56. package/conformance/invalid/agent-description-too-long.expected.json +0 -10
  57. package/conformance/invalid/agent-description-too-long.udl +0 -57
  58. package/conformance/invalid/blank-title.expected.json +0 -10
  59. package/conformance/invalid/blank-title.udl +0 -54
  60. package/conformance/invalid/call-binds-results.expected.json +0 -10
  61. package/conformance/invalid/call-binds-results.udl +0 -314
  62. package/conformance/invalid/call-unknown-action.expected.json +0 -10
  63. package/conformance/invalid/call-unknown-action.udl +0 -314
  64. package/conformance/invalid/composition-dial-duplicate-key.expected.json +0 -10
  65. package/conformance/invalid/composition-dial-duplicate-key.udl +0 -73
  66. package/conformance/invalid/depth-budget.expected.json +0 -10
  67. package/conformance/invalid/depth-budget.udl +0 -49
  68. package/conformance/invalid/duplicate-subject.expected.json +0 -10
  69. package/conformance/invalid/duplicate-subject.udl +0 -230
  70. package/conformance/invalid/forged-effects.expected.json +0 -10
  71. package/conformance/invalid/forged-effects.udl +0 -76
  72. package/conformance/invalid/format-version.expected.json +0 -10
  73. package/conformance/invalid/format-version.udl +0 -54
  74. package/conformance/invalid/instrument-id-not-snake-case.expected.json +0 -10
  75. package/conformance/invalid/instrument-id-not-snake-case.udl +0 -54
  76. package/conformance/invalid/invalid-aggregate-gate-shape.expected.json +0 -10
  77. package/conformance/invalid/invalid-aggregate-gate-shape.udl +0 -1819
  78. package/conformance/invalid/invalid-check-duration.expected.json +0 -10
  79. package/conformance/invalid/invalid-check-duration.udl +0 -219
  80. package/conformance/invalid/invalid-dial-anchor.expected.json +0 -10
  81. package/conformance/invalid/invalid-dial-anchor.udl +0 -219
  82. package/conformance/invalid/invalid-exception-parent-ref.expected.json +0 -10
  83. package/conformance/invalid/invalid-exception-parent-ref.udl +0 -2487
  84. package/conformance/invalid/invalid-exposure-shape.expected.json +0 -10
  85. package/conformance/invalid/invalid-exposure-shape.udl +0 -1819
  86. package/conformance/invalid/invalid-journeys.expected.json +0 -10
  87. package/conformance/invalid/invalid-journeys.udl +0 -77
  88. package/conformance/invalid/invalid-remainder.expected.json +0 -10
  89. package/conformance/invalid/invalid-remainder.udl +0 -220
  90. package/conformance/invalid/invalid-schema-keyword.expected.json +0 -10
  91. package/conformance/invalid/invalid-schema-keyword.udl +0 -220
  92. package/conformance/invalid/invalid-utf8.expected.json +0 -10
  93. package/conformance/invalid/invalid-utf8.udl +0 -1
  94. package/conformance/invalid/leaf-effect-mismatch.expected.json +0 -10
  95. package/conformance/invalid/leaf-effect-mismatch.udl +0 -314
  96. package/conformance/invalid/malformed-json.expected.json +0 -10
  97. package/conformance/invalid/malformed-json.udl +0 -1
  98. package/conformance/invalid/missing-create-action.expected.json +0 -10
  99. package/conformance/invalid/missing-create-action.udl +0 -48
  100. package/conformance/invalid/missing-exception-amount-field.expected.json +0 -10
  101. package/conformance/invalid/missing-exception-amount-field.udl +0 -2487
  102. package/conformance/invalid/missing-exception-contract.expected.json +0 -14
  103. package/conformance/invalid/missing-exception-contract.udl +0 -2523
  104. package/conformance/invalid/missing-exception-reason-field.expected.json +0 -10
  105. package/conformance/invalid/missing-exception-reason-field.udl +0 -2487
  106. package/conformance/invalid/not-an-object.expected.json +0 -10
  107. package/conformance/invalid/not-an-object.udl +0 -1
  108. package/conformance/invalid/payout-reconcile-not-a-bank-debit.expected.json +0 -10
  109. package/conformance/invalid/payout-reconcile-not-a-bank-debit.udl +0 -259
  110. package/conformance/invalid/piece-plan-without-partition.expected.json +0 -10
  111. package/conformance/invalid/piece-plan-without-partition.udl +0 -305
  112. package/conformance/invalid/private-action-independent-approval.expected.json +0 -10
  113. package/conformance/invalid/private-action-independent-approval.udl +0 -314
  114. package/conformance/invalid/quote-freeze-set-incomplete.expected.json +0 -10
  115. package/conformance/invalid/quote-freeze-set-incomplete.udl +0 -261
  116. package/conformance/invalid/quote-named-reference-gate.expected.json +0 -10
  117. package/conformance/invalid/quote-named-reference-gate.udl +0 -50
  118. package/conformance/invalid/reconcile-named-reference-gate.expected.json +0 -10
  119. package/conformance/invalid/reconcile-named-reference-gate.udl +0 -50
  120. package/conformance/invalid/unfund-order-not-reversed.expected.json +0 -10
  121. package/conformance/invalid/unfund-order-not-reversed.udl +0 -314
  122. package/conformance/invalid/unknown-key.expected.json +0 -10
  123. package/conformance/invalid/unknown-key.udl +0 -55
  124. package/conformance/invalid/unknown-reference-gate-field.expected.json +0 -10
  125. package/conformance/invalid/unknown-reference-gate-field.udl +0 -1819
  126. package/conformance/invalid/unknown-required-field.expected.json +0 -10
  127. package/conformance/invalid/unknown-required-field.udl +0 -220
  128. package/conformance/invalid/unreachable-state.expected.json +0 -10
  129. package/conformance/invalid/unreachable-state.udl +0 -55
  130. package/conformance/invalid/wrong-exception-amount-field.expected.json +0 -10
  131. package/conformance/invalid/wrong-exception-amount-field.udl +0 -2487
  132. package/conformance/invalid/wrong-exception-reason-field.expected.json +0 -10
  133. package/conformance/invalid/wrong-exception-reason-field.udl +0 -2487
  134. package/conformance/valid/agent-description.expected.json +0 -6
  135. package/conformance/valid/agent-description.udl +0 -66
  136. package/conformance/valid/attested.expected.json +0 -6
  137. package/conformance/valid/attested.udl +0 -251
  138. package/conformance/valid/cards.expected.json +0 -6
  139. package/conformance/valid/cards.udl +0 -1579
  140. package/conformance/valid/commerce-escrow.expected.json +0 -6
  141. package/conformance/valid/commerce-escrow.udl +0 -1512
  142. package/conformance/valid/compiled-crowdfunding.expected.json +0 -6
  143. package/conformance/valid/compiled-crowdfunding.udl +0 -1843
  144. package/conformance/valid/compiled-watch-club.expected.json +0 -6
  145. package/conformance/valid/compiled-watch-club.udl +0 -2486
  146. package/conformance/valid/complete-contract.expected.json +0 -6
  147. package/conformance/valid/complete-contract.udl +0 -218
  148. package/conformance/valid/effect-signatures.expected.json +0 -6
  149. package/conformance/valid/effect-signatures.udl +0 -75
  150. package/conformance/valid/hand-edited.expected.json +0 -6
  151. package/conformance/valid/hand-edited.udl +0 -1
  152. package/conformance/valid/insured-car-marketplace.expected.json +0 -6
  153. package/conformance/valid/insured-car-marketplace.udl +0 -1050
  154. package/conformance/valid/insured-travel.expected.json +0 -6
  155. package/conformance/valid/insured-travel.udl +0 -3469
  156. package/conformance/valid/minimal.expected.json +0 -6
  157. package/conformance/valid/minimal.udl +0 -62
  158. package/conformance/valid/piece-plan-calls.expected.json +0 -6
  159. package/conformance/valid/piece-plan-calls.udl +0 -314
  160. package/conformance/valid/protection.expected.json +0 -6
  161. package/conformance/valid/protection.udl +0 -1551
  162. package/conformance/valid/string-escaping.expected.json +0 -6
  163. package/conformance/valid/string-escaping.udl +0 -54
  164. package/conformance/valid/vocabulary.expected.json +0 -6
  165. package/conformance/valid/vocabulary.udl +0 -2012
  166. package/dist/allocation.d.ts +0 -60
  167. package/dist/allocation.d.ts.map +0 -1
  168. package/dist/allocation.js +0 -177
  169. package/dist/allocation.js.map +0 -1
  170. package/dist/check-profiles.d.ts +0 -57
  171. package/dist/check-profiles.d.ts.map +0 -1
  172. package/dist/check-profiles.js +0 -62
  173. package/dist/check-profiles.js.map +0 -1
  174. package/dist/distribution.d.ts +0 -15
  175. package/dist/distribution.d.ts.map +0 -1
  176. package/dist/distribution.js +0 -49
  177. package/dist/distribution.js.map +0 -1
  178. package/dist/effects.d.ts +0 -58
  179. package/dist/effects.d.ts.map +0 -1
  180. package/dist/effects.js +0 -1126
  181. package/dist/effects.js.map +0 -1
  182. package/dist/reference.d.ts +0 -3
  183. package/dist/reference.d.ts.map +0 -1
  184. package/dist/reference.js +0 -28
  185. package/dist/reference.js.map +0 -1
  186. package/dist/vocabulary.d.ts +0 -23
  187. package/dist/vocabulary.d.ts.map +0 -1
  188. package/dist/vocabulary.js +0 -1052
  189. package/dist/vocabulary.js.map +0 -1
  190. package/docs/funding-custody.md +0 -165
  191. package/docs/guide/01-a-document.md +0 -37
  192. package/docs/guide/02-money-steps.md +0 -23
  193. package/docs/guide/03-laws.md +0 -18
  194. package/docs/guide/04-fees-and-remainder.md +0 -36
  195. package/docs/guide/05-checks-updates-dials.md +0 -7
  196. package/docs/guide/06-effects.md +0 -11
  197. package/docs/guide/07-evolution.md +0 -11
  198. package/docs/guide/08-implementing.md +0 -13
  199. package/docs/guide/09-schedules-and-allocation.md +0 -132
  200. package/docs/llms-full.txt +0 -2008
  201. package/docs/llms.txt +0 -14
  202. package/docs/piece-plans.md +0 -148
  203. package/docs/reference/canonical.md +0 -16
  204. package/docs/reference/clauses.md +0 -1585
  205. package/docs/reference/cli.md +0 -24
  206. package/docs/reference/diagnostics.md +0 -38
  207. package/skills/udl/SKILL.md +0 -28
  208. package/src/allocation.ts +0 -259
  209. package/src/check-profiles.ts +0 -80
  210. package/src/distribution.ts +0 -61
  211. package/src/effects.ts +0 -1920
  212. package/src/reference.ts +0 -31
  213. package/src/vocabulary.ts +0 -1635
package/src/evolution.ts CHANGED
@@ -1,1054 +1,48 @@
1
- import { UDL_LIMITS } from "./limits.js";
2
- import { deriveUdlActionEffects } from "./effects.js";
3
- import type {
4
- UdlAggregate,
5
- UdlDocument,
6
- UdlInstrument,
7
- UdlAction,
8
- } from "./schema.js";
9
- import { udlClauseVocabulary } from "./schema.js";
10
- import { UdlError } from "./validation.js";
1
+ import type { UdlDocument } from "./schema.js";
11
2
  import { issue, type UdlIssue } from "./diagnostics.js";
12
3
 
13
- export interface EvolutionFieldSnapshot {
14
- readonly required: boolean;
15
- readonly schema: unknown;
16
- }
17
-
18
- export interface EvolutionStepSnapshot {
19
- readonly bind: unknown;
20
- readonly capture: unknown;
21
- readonly operation: string;
22
- }
23
-
24
- export interface EvolutionMoveSnapshot extends EvolutionStepSnapshot {
25
- readonly key: string;
26
- }
27
-
28
- export interface EvolutionActionSnapshot {
29
- readonly requiresInput?: unknown;
30
- readonly engineOwned?: unknown;
31
- readonly captureEngine?: unknown;
32
- readonly requiresAllocation?: unknown;
33
- readonly allocate?: unknown;
34
- readonly contributionStage?: unknown;
35
- readonly transitionsRefs?: unknown;
36
- /** Missing on snapshots written before receipt-input capture existed. */
37
- readonly captureInput?: unknown;
38
- readonly deadline: unknown;
39
- readonly decision: unknown;
40
- /** Missing on snapshots written before UDL completeness. */
41
- readonly principal?: string | null;
42
- /** Missing on snapshots written before quote-commit existed. */
43
- readonly commit?: string | null;
44
- /** Missing on snapshots written before UDL completeness. */
45
- readonly remainder?: unknown;
46
- /** Missing on snapshots written before UDL completeness. */
47
- readonly requiresChecks?: unknown;
48
- /** Missing on snapshots written before UDL completeness. */
49
- readonly sandboxFailurePoint?: string | null;
50
- /** Missing on snapshots written before UDL completeness. */
51
- readonly updates?: readonly string[];
52
- /** Missing on snapshots written before weighted distribution existed. */
53
- readonly distribute?: unknown;
54
- readonly due: unknown;
55
- readonly earnable: boolean;
56
- /** Missing on snapshots written before derived effect rows entered UDL. */
57
- readonly effects?: unknown;
58
- readonly eventName: string | null;
59
- readonly input: Readonly<Record<string, EvolutionFieldSnapshot>> | null;
60
- /**
61
- * Every top-level input schema keyword EXCEPT properties/required (those are
62
- * diffed field by field). additionalProperties, string and array bounds,
63
- * numeric bounds, enum, const, pattern, and format change what callers may
64
- * send, so they are frozen once live.
65
- */
66
- readonly inputConstraints: unknown;
67
- /** Missing on snapshots written before payout intents existed. */
68
- readonly payout?: unknown;
69
- readonly port: unknown;
70
- /** Missing on snapshots written before public action metadata existed. */
71
- readonly publicAction?: string | null;
72
- readonly requiresAggregate: unknown;
73
- readonly requiresDrainedAccount: unknown;
74
- /** Missing on snapshots written before exposure gates entered open UDL. */
75
- readonly requiresExposure?: unknown;
76
- readonly unique?: unknown;
77
- readonly requiresRefs: unknown;
78
- /** Missing on snapshots written before reconcile expectations existed. */
79
- readonly reconcile?: unknown;
80
- readonly setsAt: unknown;
81
- /** Missing on snapshots written before signed child sums existed. */
82
- readonly signedSum?: unknown;
83
- /** Missing on snapshots written before cascade transitions existed. */
84
- readonly cascade?: unknown;
85
- readonly moves: readonly EvolutionMoveSnapshot[];
86
- readonly steps: readonly EvolutionStepSnapshot[];
87
- }
88
-
89
- export interface EvolutionTransitionSnapshot {
90
- readonly from: readonly string[];
91
- readonly to: string;
92
- }
93
-
94
- /** The complete serializable algebra protected by append-only evolution. */
95
- export interface InstrumentEvolutionSnapshot {
96
- readonly templateBinding?: unknown;
97
- readonly allocation?: unknown;
98
- readonly contributions?: unknown;
99
- readonly dateOrder?: unknown;
100
- /** Missing on snapshots written before UDL carried authored action order. */
101
- readonly actionOrder?: readonly string[];
102
- readonly aggregateInvariants: readonly string[];
103
- /** Missing on snapshots written before UDL completeness. */
104
- readonly callerParkedStates?: Readonly<Record<string, string>>;
105
- readonly derivedAmounts: readonly string[];
106
- /** Missing on snapshots written before UDL completeness. */
107
- readonly dials?: unknown;
108
- readonly distinctParties?: true;
109
- /** Missing on snapshots written before unified fee rules entered UDL. */
110
- readonly feeRules?: unknown;
111
- readonly fields: Readonly<Record<string, EvolutionFieldSnapshot>>;
112
- readonly id: string;
113
- readonly idPrefix: string;
114
- readonly initial: string;
115
- /** Missing on snapshots written before UDL completeness. */
116
- readonly nav?: readonly string[];
117
- readonly parties: Readonly<Record<string, string>>;
118
- readonly partitions: readonly string[];
119
- readonly states: readonly string[];
120
- readonly subjects: readonly string[];
121
- /** Missing on snapshots written before UDL completeness. */
122
- readonly subjectExtensible?: boolean;
123
- /** Missing on snapshots written before UDL completeness. */
124
- readonly surfaceVisibility?: string | null;
125
- /** Missing on snapshots written before UDL completeness. */
126
- readonly templateId?: string | null;
127
- readonly transitions: Readonly<Record<string, EvolutionTransitionSnapshot>>;
128
- /** Every quote-commit pair, keyed by quoting action. */
129
- readonly quotes: unknown;
130
- readonly updateFields: readonly string[];
131
- /** Missing on snapshots written before UDL completeness. */
132
- readonly updateExamples?: unknown;
133
- readonly updateStates: readonly string[];
134
- readonly actions: Readonly<Record<string, EvolutionActionSnapshot>>;
135
- }
136
-
137
- /** Project an open .udl instrument into the evolution engine's exact semantic shape. */
138
- export function snapshotUdlInstrument(
139
- instrument: UdlInstrument,
140
- ): InstrumentEvolutionSnapshot {
141
- const required = new Set(instrument.required);
142
- return {
143
- templateBinding: instrument.templateBinding ?? null,
144
- allocation: instrument.allocation ?? null,
145
- contributions: instrument.contributions ?? null,
146
- dateOrder: instrument.dateOrder ?? [],
147
- actionOrder: [...instrument.actionOrder],
148
- aggregateInvariants: (instrument.aggregateInvariants ?? []).map(
149
- aggregateInvariantKey,
150
- ),
151
- callerParkedStates: instrument.callerParkedStates ?? {},
152
- derivedAmounts: (instrument.derivedAmounts ?? []).map((amount) =>
153
- amount.rule.kind === "minimum"
154
- ? `${amount.field}=min(${amount.sourceField},${amount.rule.capField})`
155
- : `${amount.field}=floor(${amount.sourceField}*${typeof amount.rule.bps === "number" ? amount.rule.bps : `fields.${amount.rule.bps.field}`}/10000)`,
156
- ),
157
- dials: instrument.dials ?? [],
158
- ...(instrument.distinctParties ? { distinctParties: true as const } : {}),
159
- ...(instrument.feeRules
160
- ? {
161
- feeRules: instrument.feeRules.map((fee) => ({
162
- ...fee,
163
- rule:
164
- fee.rule.kind === "tiered"
165
- ? {
166
- ...fee.rule,
167
- tiers: fee.rule.tiers.map((tier) => ({
168
- ...tier,
169
- rule: { ...tier.rule },
170
- })),
171
- }
172
- : { ...fee.rule },
173
- })),
174
- }
175
- : {}),
176
- fields: Object.fromEntries(
177
- Object.entries(instrument.fields).map(([field, schema]) => [
178
- field,
179
- { required: required.has(field), schema: withoutProse(schema) },
180
- ]),
181
- ),
182
- id: instrument.id,
183
- idPrefix: instrument.idPrefix,
184
- initial: instrument.lifecycle.initial,
185
- nav: instrument.nav ?? [],
186
- parties: compactStringRecord(instrument.parties),
187
- partitions: (instrument.partitions ?? []).map(
188
- (partition) =>
189
- `${partition.totalField} = ${[...partition.pieceFields].sort().join(" + ")}`,
190
- ),
191
- states: [...instrument.lifecycle.states],
192
- subjects: instrument.subject ? [...instrument.subject.kinds] : [],
193
- subjectExtensible: instrument.subject?.extensible ?? false,
194
- surfaceVisibility: instrument.surfaceVisibility ?? null,
195
- templateId: instrument.templateId ?? null,
196
- transitions: Object.fromEntries(
197
- Object.entries(instrument.lifecycle.transitions).map(
198
- ([action, transition]) => [
199
- action,
200
- { from: [...transition.from], to: transition.to },
201
- ],
202
- ),
203
- ),
204
- quotes: Object.fromEntries(
205
- Object.entries(instrument.actions)
206
- .filter(([, action]) => action.quote)
207
- .map(([name, action]) => [name, action.quote]),
208
- ),
209
- updateFields: instrument.update?.fields ?? [],
210
- updateExamples: instrument.update?.examples ?? null,
211
- updateStates: instrument.update?.states ?? [],
212
- actions: Object.fromEntries(
213
- instrument.actionOrder.map((action) => [
214
- action,
215
- snapshotUdlAction(instrument.actions[action]!),
216
- ]),
217
- ),
218
- };
219
- }
220
-
221
- /**
222
- * Compare two product definitions under the append-only evolution law.
223
- *
224
- * Both documents must already have passed `validateUdl`. The diff walks
225
- * straight into `stableStringify`, so an unvalidated document with a cycle in
226
- * it throws a RangeError out of here where the validator would have returned a
227
- * clean `resource_limit` issue: parse first, then diff.
228
- */
229
- export function diffValidatedUdlEvolution(
230
- previous: UdlDocument,
231
- next: UdlDocument,
232
- ): readonly UdlIssue[] {
233
- return diffValidatedUdlEvolutionMessages(previous, next).map((message) => {
234
- const instrumentId = /^([^:]+): /.exec(message)?.[1];
235
- const instrumentIndex = previous.instruments.findIndex(
236
- (instrument) => instrument.id === instrumentId,
4
+ function stable(value: unknown): string {
5
+ if (Array.isArray(value)) return "[" + value.map(stable).join(",") + "]";
6
+ if (value && typeof value === "object")
7
+ return (
8
+ "{" +
9
+ Object.entries(value)
10
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
11
+ .map(([key, entry]) => JSON.stringify(key) + ":" + stable(entry))
12
+ .join(",") +
13
+ "}"
237
14
  );
238
- const base =
239
- instrumentIndex < 0
240
- ? "$.instruments"
241
- : `$.instruments[${instrumentIndex}]`;
242
- return evolutionIssue(message, base);
243
- });
15
+ return JSON.stringify(value) ?? "undefined";
244
16
  }
245
17
 
246
- function diffValidatedUdlEvolutionMessages(
247
- previous: UdlDocument,
18
+ /** Existing instruments are frozen; a live document may add independent instruments. */
19
+ export function diffValidatedUdlEvolution(
20
+ live: UdlDocument,
248
21
  next: UdlDocument,
249
- ): readonly string[] {
250
- const violations: string[] = [];
251
- if (previous.product !== next.product) {
252
- violations.push(
253
- `product id changed from ${previous.product} to ${next.product}`,
254
- );
255
- }
256
- if (next.version < previous.version) {
257
- violations.push(
258
- `product version moved backward from ${previous.version} to ${next.version}`,
259
- );
260
- }
261
-
262
- let semanticChange = false;
263
- const previousSubjects = new Map(
264
- previous.subjects.map((subject) => [subject.kind, subject] as const),
265
- );
266
- const nextSubjects = new Map(
267
- next.subjects.map((subject) => [subject.kind, subject] as const),
268
- );
269
- for (const [kind, subject] of previousSubjects) {
270
- const current = nextSubjects.get(kind);
271
- if (!current) {
272
- semanticChange = true;
273
- violations.push(`subject kind ${kind} was removed or renamed`);
274
- } else if (stableStringify(subject) !== stableStringify(current)) {
275
- semanticChange = true;
276
- violations.push(`subject kind ${kind} changed after becoming live`);
277
- }
278
- }
279
- for (const kind of nextSubjects.keys()) {
280
- if (!previousSubjects.has(kind)) semanticChange = true;
281
- }
282
-
283
- const previousInstruments = new Map(
284
- previous.instruments.map(
285
- (instrument) =>
286
- [instrument.id, snapshotUdlInstrument(instrument)] as const,
287
- ),
288
- );
289
- const nextInstruments = new Map(
290
- next.instruments.map(
291
- (instrument) =>
292
- [instrument.id, snapshotUdlInstrument(instrument)] as const,
293
- ),
294
- );
295
- for (const [id, instrument] of previousInstruments) {
296
- const current = nextInstruments.get(id);
297
- if (!current) {
298
- semanticChange = true;
299
- violations.push(`${id}: live instrument was removed from the product`);
300
- continue;
301
- }
302
- if (stableStringify(instrument) !== stableStringify(current)) {
303
- semanticChange = true;
304
- }
305
- violations.push(...diffInstrumentEvolutionMessages(instrument, current));
306
- }
307
- for (const id of nextInstruments.keys()) {
308
- if (!previousInstruments.has(id)) semanticChange = true;
309
- }
310
-
311
- if (semanticChange && next.version <= previous.version) {
312
- violations.push(
313
- `product definition changed without increasing version ${previous.version}`,
314
- );
315
- }
316
- return violations;
317
- }
318
-
319
- /** Diff one instrument snapshot. An empty result means the change is additive. */
320
- export function diffInstrumentEvolution(
321
- previous: InstrumentEvolutionSnapshot,
322
- next: InstrumentEvolutionSnapshot,
323
- ): readonly UdlIssue[] {
324
- return diffInstrumentEvolutionMessages(previous, next).map((message) =>
325
- evolutionIssue(message, "$.instruments"),
326
- );
327
- }
328
-
329
- function diffInstrumentEvolutionMessages(
330
- previous: InstrumentEvolutionSnapshot,
331
- next: InstrumentEvolutionSnapshot,
332
- ): readonly string[] {
333
- if (previous.id !== next.id) {
334
- return [`instrument id changed from ${previous.id} to ${next.id}`];
335
- }
336
- const violations: string[] = [];
337
- if (previous.idPrefix !== next.idPrefix) {
338
- violations.push(
339
- `instance id prefix changed from ${previous.idPrefix} to ${next.idPrefix}`,
340
- );
341
- }
342
- if (previous.initial !== next.initial) {
343
- violations.push(
344
- `initial lifecycle state changed from ${previous.initial} to ${next.initial}`,
345
- );
346
- }
347
-
348
- violations.push(
349
- ...removedSetEntries(
350
- previous.states,
351
- next.states,
352
- (state) => `lifecycle state ${state} was removed or renamed`,
353
- ),
354
- );
355
- for (const [action, transition] of Object.entries(previous.transitions)) {
356
- const current = next.transitions[action];
357
- if (!current) {
358
- violations.push(`transition for action ${action} was removed or renamed`);
359
- continue;
360
- }
361
- if (transition.to !== current.to) {
362
- violations.push(
363
- `transition for action ${action} changed its target state from ${transition.to} to ${current.to}`,
364
- );
365
- }
366
- violations.push(
367
- ...removedSetEntries(
368
- transition.from,
369
- current.from,
370
- (state) =>
371
- `transition for action ${action} no longer fires from state ${state}`,
372
- ),
373
- );
374
- }
375
-
376
- violations.push(...diffFields(previous.fields, next.fields));
377
- if (Boolean(previous.distinctParties) !== Boolean(next.distinctParties)) {
378
- violations.push("distinct-party admission changed after becoming live");
379
- }
380
- if (previous.actionOrder) {
381
- const previousActions = new Set(previous.actionOrder);
382
- const retainedActionOrder = (next.actionOrder ?? []).filter((action) =>
383
- previousActions.has(action),
384
- );
385
- if (
386
- stableStringify(previous.actionOrder) !==
387
- stableStringify(retainedActionOrder)
388
- ) {
389
- violations.push("instrument action order changed after becoming live");
390
- }
391
- }
392
- violations.push(...diffActions(previous.actions, next.actions));
393
- violations.push(...diffParties(previous.parties, next.parties));
394
- for (const key of [
395
- "allocation",
396
- "contributions",
397
- "dateOrder",
398
- "templateBinding",
399
- ] as const) {
400
- if (
401
- stableStringify(previous[key] ?? (key === "dateOrder" ? [] : null)) !==
402
- stableStringify(next[key] ?? (key === "dateOrder" ? [] : null))
403
- )
404
- violations.push(`${key} contract changed`);
405
- }
406
- violations.push(...diffAggregates(previous, next));
407
- if (
408
- stableStringify(Object.keys(previous.callerParkedStates ?? {}).sort()) !==
409
- stableStringify(Object.keys(next.callerParkedStates ?? {}).sort())
410
- ) {
411
- violations.push("caller-parked state annotations changed");
412
- }
413
- if (
414
- stableStringify(previous.dials ?? []) !== stableStringify(next.dials ?? [])
415
- ) {
416
- violations.push("instrument dials changed after becoming live");
417
- }
418
- if (
419
- (previous.surfaceVisibility ?? null) !== (next.surfaceVisibility ?? null)
420
- ) {
421
- violations.push(
422
- "instrument surface visibility changed after becoming live",
423
- );
424
- }
425
- if ((previous.templateId ?? null) !== (next.templateId ?? null)) {
426
- violations.push(
427
- "instrument archetype template changed after becoming live",
428
- );
429
- }
430
- if (previous.subjectExtensible && !next.subjectExtensible) {
431
- violations.push("tenant-defined subject kinds are no longer accepted");
432
- }
433
- if (
434
- stableStringify(previous.derivedAmounts ?? []) !==
435
- stableStringify(next.derivedAmounts ?? [])
436
- ) {
437
- violations.push(
438
- "derived amount rules changed; derived money arithmetic is frozen once live",
439
- );
440
- }
441
- if (
442
- stableStringify(previous.feeRules ?? []) !==
443
- stableStringify(next.feeRules ?? [])
444
- ) {
445
- violations.push(
446
- "feeRules changed; fee calculation and settlement funding are frozen once live",
447
- );
448
- }
449
- violations.push(
450
- ...removedSetEntries(
451
- previous.partitions,
452
- next.partitions,
453
- (law) => `partition law [${law}] was removed`,
454
- ),
455
- ...next.partitions
456
- .filter((law) => !previous.partitions.includes(law))
457
- .map(
458
- (law) =>
459
- `partition law [${law}] was added, which can reject existing callers`,
22
+ ): UdlIssue[] {
23
+ const issues: UdlIssue[] = [];
24
+ const refuse = (path: string, message: string) =>
25
+ issues.push(issue("UDL7001", path, message));
26
+ if (live.product !== next.product || live.currency !== next.currency)
27
+ refuse("$", "product identity and currency are immutable");
28
+ for (const [name, party] of Object.entries(live.parties))
29
+ if (stable(party) !== stable(next.parties[name]))
30
+ refuse(`$.parties.${name}`, "a live party cannot change or disappear");
31
+ for (const instrument of live.instruments) {
32
+ const updated = next.instruments.find((i) => i.id === instrument.id);
33
+ if (!updated || stable(instrument) !== stable(updated))
34
+ refuse(
35
+ `$.instruments.${instrument.id}`,
36
+ "a live instrument cannot change or disappear; recreate development estates",
37
+ );
38
+ }
39
+ if (stable(live) !== stable(next) && next.version <= live.version)
40
+ issues.push(
41
+ issue(
42
+ "UDL7002",
43
+ "$.version",
44
+ "a changed document needs a higher version",
460
45
  ),
461
- );
462
- violations.push(...diffQuotes(previous.quotes, next.quotes));
463
- violations.push(
464
- ...removedSetEntries(
465
- previous.subjects,
466
- next.subjects,
467
- (kind) => `subject kind ${kind} was removed, rejecting linked instances`,
468
- ),
469
- ...removedSetEntries(
470
- previous.updateFields,
471
- next.updateFields,
472
- (field) => `update policy no longer permits field ${field}`,
473
- ),
474
- ...removedSetEntries(
475
- previous.updateStates,
476
- next.updateStates,
477
- (state) => `update policy no longer permits state ${state}`,
478
- ),
479
- );
480
- return violations.map((violation) => `${next.id}: ${violation}`);
481
- }
482
-
483
- function evolutionIssue(message: string, instrumentBase: string): UdlIssue {
484
- if (message.startsWith("product id changed"))
485
- return issue("UDL7001", "$.product", message);
486
- if (message.startsWith("product version moved backward"))
487
- return issue("UDL7001", "$.version", message);
488
- if (message.startsWith("product definition changed"))
489
- return issue("UDL7002", "$.version", message);
490
- if (message.startsWith("subject kind "))
491
- return issue("UDL7001", "$.subjects", message);
492
-
493
- const instrumentMatch = /^([^:]+): (.*)$/.exec(message);
494
- const detail = instrumentMatch?.[2] ?? message;
495
- const transitionAction = /^transition for action ([a-z][a-z0-9_]*)/.exec(
496
- detail,
497
- )?.[1];
498
- if (transitionAction)
499
- return issue(
500
- "UDL7001",
501
- `${instrumentBase}.lifecycle.transitions.${transitionAction}`,
502
- message,
503
- );
504
- const action = /^action ([a-z][a-z0-9_]*)/.exec(detail)?.[1];
505
- if (action)
506
- return issue("UDL7001", `${instrumentBase}.actions.${action}`, message);
507
- if (detail.includes("instrument id"))
508
- return issue("UDL7001", instrumentBase, message);
509
- if (
510
- detail.includes("id prefix") ||
511
- detail.includes("lifecycle") ||
512
- detail.includes("transition") ||
513
- detail.includes("initial") ||
514
- detail.includes("live instrument was removed")
515
- )
516
- return issue("UDL7001", `${instrumentBase}.lifecycle`, message);
517
- if (detail.startsWith("field "))
518
- return issue("UDL7001", `${instrumentBase}.fields`, message);
519
- if (
520
- detail.includes("move") ||
521
- detail.includes("payout") ||
522
- detail.includes("fee") ||
523
- detail.includes("quote") ||
524
- detail.includes("derived amount") ||
525
- detail.includes("partition") ||
526
- detail.includes("distribution") ||
527
- detail.includes("signed child sum") ||
528
- detail.includes("earnable")
529
- )
530
- return issue("UDL7001", `${instrumentBase}.actions`, message);
531
- if (
532
- detail.includes("gate") ||
533
- detail.includes("check prerequisite") ||
534
- detail.includes("reconcile expectation") ||
535
- detail.includes("drained-account") ||
536
- detail.includes("exposure") ||
537
- detail.includes("aggregate")
538
- )
539
- return issue("UDL7001", `${instrumentBase}.actions`, message);
540
- if (
541
- detail.startsWith("action ") ||
542
- detail.includes("action order") ||
543
- detail.includes("step count")
544
- )
545
- return issue("UDL7001", `${instrumentBase}.actions`, message);
546
- return issue("UDL7001", instrumentBase, message);
547
- }
548
-
549
- function snapshotUdlAction(definition: UdlAction): EvolutionActionSnapshot {
550
- return {
551
- requiresInput: definition.requiresInput ?? null,
552
- engineOwned: definition.engineOwned ?? null,
553
- captureEngine: definition.captureEngine ?? null,
554
- requiresAllocation: definition.requiresAllocation ?? null,
555
- allocate: definition.allocate ?? null,
556
- contributionStage: definition.contributionStage ?? null,
557
- transitionsRefs: definition.transitionsRefs ?? [],
558
- captureInput: definition.captureInput ?? null,
559
- commit: definition.commit ?? null,
560
- deadline: definition.deadline ?? null,
561
- decision: definition.decision ?? null,
562
- principal: definition.principal ?? null,
563
- remainder: definition.remainder ?? null,
564
- requiresChecks: definition.requiresChecks ?? [],
565
- sandboxFailurePoint: definition.sandboxFailurePoint ?? null,
566
- updates: definition.updates ?? [],
567
- distribute: definition.distribute ?? null,
568
- due: definition.due ?? null,
569
- earnable: definition.earnable ?? false,
570
- effects:
571
- definition.effects ??
572
- deriveUdlActionEffects(definition, udlClauseVocabulary),
573
- eventName: definition.eventName ?? null,
574
- input: definition.input ? snapshotJsonSchemaFields(definition.input) : null,
575
- inputConstraints: definition.input
576
- ? snapshotJsonSchemaConstraints(definition.input)
577
- : null,
578
- payout: definition.payout ?? null,
579
- port: definition.port ?? null,
580
- publicAction: definition.publicAction ?? null,
581
- setsAt: definition.setsAt ?? null,
582
- signedSum: definition.signedSum ?? null,
583
- cascade: definition.cascade
584
- ? definition.cascade.map((entry) => ({ ...entry }))
585
- : null,
586
- moves: definition.moves.map((move) => ({
587
- bind: move.bind,
588
- capture: move.capture ?? {},
589
- key: move.key,
590
- operation: move.operation,
591
- })),
592
- requiresAggregate: definition.requiresAggregate ?? [],
593
- requiresDrainedAccount: definition.requiresDrainedAccount ?? null,
594
- requiresExposure: definition.requiresExposure ?? [],
595
- unique: definition.unique ?? null,
596
- requiresRefs: definition.requiresRefs ?? [],
597
- reconcile: definition.reconcile ?? null,
598
- steps: definition.steps.map((step) => ({
599
- bind: step.bind,
600
- capture: step.capture ?? {},
601
- operation: step.operation,
602
- })),
603
- };
604
- }
605
-
606
- function snapshotJsonSchemaFields(
607
- schema: Readonly<Record<string, unknown>>,
608
- ): Readonly<Record<string, EvolutionFieldSnapshot>> {
609
- const properties = recordValue(schema.properties);
610
- const required = new Set(
611
- Array.isArray(schema.required)
612
- ? schema.required.filter(
613
- (value): value is string => typeof value === "string",
614
- )
615
- : [],
616
- );
617
- return Object.fromEntries(
618
- Object.entries(properties).map(([field, fieldSchema]) => [
619
- field,
620
- { required: required.has(field), schema: withoutProse(fieldSchema) },
621
- ]),
622
- );
623
- }
624
-
625
- /**
626
- * Descriptions are prose for readers, not shape: a wording fix in a std
627
- * template must land on a live flow as an extension, never as a refusal.
628
- */
629
- function withoutProse(value: unknown, depth = 1): unknown {
630
- if (depth > UDL_LIMITS.maxDepth) throw nestingExceeded();
631
- if (Array.isArray(value)) {
632
- return value.map((item) => withoutProse(item, depth + 1));
633
- }
634
- if (value === null || typeof value !== "object") return value;
635
- return Object.fromEntries(
636
- Object.entries(value as Record<string, unknown>)
637
- .filter(([key]) => key !== "description")
638
- .map(([key, entry]) => [key, withoutProse(entry, depth + 1)]),
639
- );
640
- }
641
-
642
- function nestingExceeded(): UdlError {
643
- return new UdlError([
644
- issue("UDL1004", "$", `UDL nesting exceeds ${UDL_LIMITS.maxDepth} levels`),
645
- ]);
646
- }
647
-
648
- function snapshotJsonSchemaConstraints(
649
- schema: Readonly<Record<string, unknown>>,
650
- ): Readonly<Record<string, unknown>> {
651
- const {
652
- properties: _properties,
653
- required: _required,
654
- ...constraints
655
- } = schema;
656
- return constraints;
657
- }
658
-
659
- function diffFields(
660
- previous: Readonly<Record<string, EvolutionFieldSnapshot>>,
661
- next: Readonly<Record<string, EvolutionFieldSnapshot>>,
662
- ): string[] {
663
- const violations: string[] = [];
664
- for (const [field, descriptor] of Object.entries(previous)) {
665
- const current = next[field];
666
- if (!current) {
667
- violations.push(`field ${field} was removed or renamed`);
668
- continue;
669
- }
670
- if (!descriptor.required && current.required) {
671
- violations.push(`field ${field} became required, tightening the schema`);
672
- }
673
- if (
674
- stableStringify(descriptor.schema) !== stableStringify(current.schema)
675
- ) {
676
- violations.push(
677
- `field ${field} schema changed; a live field schema is frozen`,
678
- );
679
- }
680
- }
681
- for (const [field, descriptor] of Object.entries(next)) {
682
- if (!previous[field] && descriptor.required) {
683
- violations.push(
684
- `field ${field} was added as required, which rejects existing instances (only optional fields are additive)`,
685
- );
686
- }
687
- }
688
- return violations;
689
- }
690
-
691
- function diffActions(
692
- previous: Readonly<Record<string, EvolutionActionSnapshot>>,
693
- next: Readonly<Record<string, EvolutionActionSnapshot>>,
694
- ): string[] {
695
- const violations: string[] = [];
696
- for (const [action, descriptor] of Object.entries(previous)) {
697
- const current = next[action];
698
- if (!current) {
699
- violations.push(`action ${action} was removed or renamed`);
700
- continue;
701
- }
702
- if (descriptor.moves.length !== current.moves.length) {
703
- violations.push(
704
- `action ${action} changed its move count from ${descriptor.moves.length} to ${current.moves.length}; money movement is frozen once live`,
705
- );
706
- } else {
707
- descriptor.moves.forEach((move, index) => {
708
- const nextMove = current.moves[index] as EvolutionMoveSnapshot;
709
- if (stableStringify(move) !== stableStringify(nextMove)) {
710
- violations.push(
711
- `action ${action} changed move ${move.key} at index ${index}; money movement is frozen once live`,
712
- );
713
- }
714
- });
715
- }
716
- if (descriptor.steps.length !== current.steps.length) {
717
- violations.push(
718
- `action ${action} changed its step count from ${descriptor.steps.length} to ${current.steps.length}; a live action's steps are frozen`,
719
- );
720
- } else {
721
- descriptor.steps.forEach((step, index) => {
722
- const nextStep = current.steps[index] as EvolutionStepSnapshot;
723
- if (stableStringify(step) === stableStringify(nextStep)) return;
724
- violations.push(
725
- `action ${action} changed step ${index} (${step.operation}); a live action's steps are frozen`,
726
- );
727
- });
728
- }
729
- violations.push(
730
- ...diffInput(action, descriptor.input ?? {}, current.input ?? {}),
731
- );
732
- // A action gaining its first input is additive (per-field checks still catch
733
- // added-as-required); once an input is live, its schema envelope is frozen.
734
- if (
735
- descriptor.input !== null &&
736
- stableStringify(descriptor.inputConstraints) !==
737
- stableStringify(current.inputConstraints)
738
- ) {
739
- violations.push(
740
- `action ${action} changed its input schema beyond declared fields; a live action input is frozen`,
741
- );
742
- }
743
- if (
744
- stableStringify(descriptor.requiresRefs) !==
745
- stableStringify(current.requiresRefs)
746
- ) {
747
- violations.push(`action ${action} changed its cross-instrument gates`);
748
- }
749
- if (
750
- stableStringify(descriptor.requiresAggregate) !==
751
- stableStringify(current.requiresAggregate)
752
- ) {
753
- violations.push(`action ${action} changed its aggregate admission gates`);
754
- }
755
- if (
756
- stableStringify(descriptor.requiresDrainedAccount) !==
757
- stableStringify(current.requiresDrainedAccount)
758
- ) {
759
- violations.push(`action ${action} changed its drained-account gate`);
760
- }
761
- if (
762
- stableStringify(descriptor.unique ?? null) !==
763
- stableStringify(current.unique ?? null)
764
- ) {
765
- violations.push(`action ${action} changed its subject uniqueness claim`);
766
- }
767
- if (
768
- descriptor.requiresExposure !== undefined &&
769
- stableStringify(descriptor.requiresExposure) !==
770
- stableStringify(current.requiresExposure)
771
- ) {
772
- violations.push(`action ${action} changed its exposure admission gates`);
773
- }
774
- if (
775
- descriptor.reconcile !== undefined &&
776
- stableStringify(descriptor.reconcile) !==
777
- stableStringify(current.reconcile)
778
- ) {
779
- violations.push(`action ${action} changed its reconcile expectations`);
780
- }
781
- if (
782
- stableStringify(descriptor.payout ?? null) !==
783
- stableStringify(current.payout ?? null)
784
- ) {
785
- violations.push(`action ${action} changed its payout intent`);
786
- }
787
- for (const key of [
788
- "requiresInput",
789
- "engineOwned",
790
- "captureEngine",
791
- "requiresAllocation",
792
- "allocate",
793
- "contributionStage",
794
- "transitionsRefs",
795
- ] as const) {
796
- if (
797
- stableStringify(
798
- descriptor[key] ?? (key === "transitionsRefs" ? [] : null),
799
- ) !==
800
- stableStringify(current[key] ?? (key === "transitionsRefs" ? [] : null))
801
- )
802
- violations.push(`action ${action} changed its ${key} contract`);
803
- }
804
- if (descriptor.earnable !== current.earnable) {
805
- violations.push(`action ${action} changed its earnable flag`);
806
- }
807
- if (
808
- descriptor.effects !== undefined &&
809
- stableStringify(descriptor.effects) !== stableStringify(current.effects)
810
- ) {
811
- violations.push(`action ${action} changed its derived effects`);
812
- }
813
- if (descriptor.eventName !== current.eventName) {
814
- violations.push(`action ${action} changed its event name`);
815
- }
816
- if (stableStringify(descriptor.due) !== stableStringify(current.due)) {
817
- violations.push(`action ${action} changed its due condition`);
818
- }
819
- if (
820
- stableStringify(descriptor.deadline) !== stableStringify(current.deadline)
821
- ) {
822
- violations.push(`action ${action} changed its admission deadline`);
823
- }
824
- if (
825
- stableStringify(descriptor.decision) !== stableStringify(current.decision)
826
- ) {
827
- violations.push(`action ${action} changed its provider decision`);
828
- }
829
- if ((descriptor.principal ?? null) !== (current.principal ?? null)) {
830
- violations.push(`action ${action} changed its caller principal`);
831
- }
832
- if (
833
- stableStringify(descriptor.remainder ?? null) !==
834
- stableStringify(current.remainder ?? null)
835
- ) {
836
- violations.push(`action ${action} changed its remainder rule`);
837
- }
838
- if (
839
- stableStringify(descriptor.requiresChecks ?? []) !==
840
- stableStringify(current.requiresChecks ?? [])
841
- ) {
842
- violations.push(`action ${action} changed its check prerequisites`);
843
- }
844
- // A failure point only adds a sandbox refusal for one documented amount, so
845
- // declaring one on an action that had none is additive; changing or
846
- // removing a declared one rewrites what a sandbox integration relies on.
847
- if (
848
- descriptor.sandboxFailurePoint &&
849
- descriptor.sandboxFailurePoint !== (current.sandboxFailurePoint ?? null)
850
- ) {
851
- violations.push(`action ${action} changed its sandbox failure point`);
852
- }
853
- if (
854
- stableStringify(descriptor.updates ?? []) !==
855
- stableStringify(current.updates ?? [])
856
- ) {
857
- violations.push(`action ${action} changed its field updates`);
858
- }
859
- if (
860
- stableStringify(descriptor.distribute) !==
861
- stableStringify(current.distribute)
862
- ) {
863
- violations.push(
864
- `action ${action} changed its distribution rule; money distribution is frozen once live`,
865
- );
866
- }
867
- if (
868
- descriptor.captureInput !== undefined &&
869
- stableStringify(descriptor.captureInput) !==
870
- stableStringify(current.captureInput)
871
- ) {
872
- violations.push(`action ${action} changed its captured receipt input`);
873
- }
874
- if (stableStringify(descriptor.port) !== stableStringify(current.port)) {
875
- violations.push(`action ${action} changed its decision port`);
876
- }
877
- if (
878
- descriptor.publicAction != null &&
879
- descriptor.publicAction !== current.publicAction
880
- ) {
881
- violations.push(`action ${action} changed its public action`);
882
- }
883
- if (
884
- stableStringify(descriptor.setsAt) !== stableStringify(current.setsAt)
885
- ) {
886
- violations.push(`action ${action} changed its computed timestamp`);
887
- }
888
- if (
889
- descriptor.signedSum !== undefined &&
890
- stableStringify(descriptor.signedSum) !==
891
- stableStringify(current.signedSum)
892
- ) {
893
- violations.push(`action ${action} changed its signed child sum`);
894
- }
895
- if (
896
- descriptor.cascade !== undefined &&
897
- stableStringify(descriptor.cascade) !== stableStringify(current.cascade)
898
- ) {
899
- violations.push(`action ${action} changed its cascade rules`);
900
- }
901
- }
902
- for (const [action, descriptor] of Object.entries(next)) {
903
- if (previous[action] || descriptor.payout == null) continue;
904
- violations.push(
905
- `action ${action} added a payout intent; external money movement is frozen once live`,
906
46
  );
907
- }
908
- return violations;
909
- }
910
-
911
- function diffInput(
912
- action: string,
913
- previous: Readonly<Record<string, EvolutionFieldSnapshot>>,
914
- next: Readonly<Record<string, EvolutionFieldSnapshot>>,
915
- ): string[] {
916
- const violations: string[] = [];
917
- for (const [field, descriptor] of Object.entries(previous)) {
918
- const current = next[field];
919
- if (!current) {
920
- // Compiled action inputs are strict objects, so a removed field starts
921
- // rejecting every caller that still sends it -- same law as instrument fields.
922
- violations.push(
923
- `action ${action} input field ${field} was removed or renamed`,
924
- );
925
- continue;
926
- }
927
- if (!descriptor.required && current.required) {
928
- violations.push(
929
- `action ${action} input field ${field} became required, tightening the action input`,
930
- );
931
- }
932
- if (
933
- stableStringify(descriptor.schema) !== stableStringify(current.schema)
934
- ) {
935
- violations.push(
936
- `action ${action} input field ${field} schema changed; a live action input schema is frozen`,
937
- );
938
- }
939
- }
940
- for (const [field, descriptor] of Object.entries(next)) {
941
- if (!previous[field] && descriptor.required) {
942
- violations.push(
943
- `action ${action} input field ${field} was added as required, tightening the action input`,
944
- );
945
- }
946
- }
947
- return violations;
948
- }
949
-
950
- function diffParties(
951
- previous: Readonly<Record<string, string>>,
952
- next: Readonly<Record<string, string>>,
953
- ): string[] {
954
- const violations: string[] = [];
955
- for (const [role, field] of Object.entries(previous)) {
956
- const current = next[role];
957
- if (!current) violations.push(`party role ${role} was removed or renamed`);
958
- else if (current !== field) {
959
- violations.push(
960
- `party role ${role} moved from field ${field} to ${current}`,
961
- );
962
- }
963
- }
964
- return violations;
965
- }
966
-
967
- function diffAggregates(
968
- previous: InstrumentEvolutionSnapshot,
969
- next: InstrumentEvolutionSnapshot,
970
- ): string[] {
971
- return [
972
- ...removedSetEntries(
973
- previous.aggregateInvariants,
974
- next.aggregateInvariants,
975
- (key) => `aggregate invariant [${key}] was removed`,
976
- ),
977
- ...next.aggregateInvariants
978
- .filter((key) => !previous.aggregateInvariants.includes(key))
979
- .map(
980
- (key) =>
981
- `aggregate invariant [${key}] was added, which can reject existing instances`,
982
- ),
983
- ];
984
- }
985
-
986
- function diffQuotes(previous: unknown, next: unknown): string[] {
987
- if (previous === null || previous === undefined) return [];
988
- if (next === null || next === undefined) return ["quote policy was removed"];
989
- return stableStringify(previous) === stableStringify(next)
990
- ? []
991
- : [
992
- "quote policy changed; the charge schedule, the frozen fields, and the refund destination are frozen once live",
993
- ];
994
- }
995
-
996
- function aggregateInvariantKey(invariant: UdlAggregate): string {
997
- const measure =
998
- "childField" in invariant
999
- ? invariant.childField
1000
- : `count${invariant.window ? `[${invariant.window.field} per ${invariant.window.days}d]` : ""}`;
1001
- return `${invariant.childInstrumentId}.${measure} within ${invariant.parentField} via ${invariant.childRefField} while ${[...invariant.childStatuses].sort().join(",")}`;
1002
- }
1003
-
1004
- function removedSetEntries(
1005
- previous: readonly string[],
1006
- next: readonly string[],
1007
- message: (entry: string) => string,
1008
- ): string[] {
1009
- const current = new Set(next);
1010
- return previous.filter((entry) => !current.has(entry)).map(message);
1011
- }
1012
-
1013
- function compactStringRecord(
1014
- value: Readonly<Record<string, string | undefined>> | undefined,
1015
- ): Readonly<Record<string, string>> {
1016
- return Object.fromEntries(
1017
- Object.entries(value ?? {}).filter(
1018
- (entry): entry is [string, string] => typeof entry[1] === "string",
1019
- ),
1020
- );
1021
- }
1022
-
1023
- function recordValue(value: unknown): Readonly<Record<string, unknown>> {
1024
- return value !== null && typeof value === "object" && !Array.isArray(value)
1025
- ? (value as Readonly<Record<string, unknown>>)
1026
- : {};
1027
- }
1028
-
1029
- /**
1030
- * The comparison key every diff above is written against. It recurses, so it
1031
- * carries the same depth budget the validator applies to a document: a
1032
- * snapshot is never deeper than the document it came from, and the deepest
1033
- * conformance document reaches 12 of the 24 levels. A cycle is infinite depth
1034
- * and lands here rather than exhausting the call stack.
1035
- */
1036
- function stableStringify(value: unknown, depth = 1): string {
1037
- if (depth > UDL_LIMITS.maxDepth) throw nestingExceeded();
1038
- if (value === null || typeof value !== "object") {
1039
- return JSON.stringify(value) ?? "null";
1040
- }
1041
- if (Array.isArray(value)) {
1042
- // An arrow, not a bare reference: `map` would pass the index as the depth.
1043
- return `[${value.map((item) => stableStringify(item, depth + 1)).join(",")}]`;
1044
- }
1045
- const entries = Object.entries(value as Record<string, unknown>)
1046
- .filter(([, entry]) => entry !== undefined)
1047
- .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
1048
- return `{${entries
1049
- .map(
1050
- ([key, entry]) =>
1051
- `${JSON.stringify(key)}:${stableStringify(entry, depth + 1)}`,
1052
- )
1053
- .join(",")}}`;
47
+ return issues;
1054
48
  }