@hyperscale0/udl 1.0.0-beta.1 → 1.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 (179) hide show
  1. package/CHANGELOG.md +78 -25
  2. package/README.md +37 -99
  3. package/conformance/README.md +44 -41
  4. package/conformance/evolution/action-contract.expected.json +10 -0
  5. package/conformance/evolution/action-contract.live.udl +44 -0
  6. package/conformance/evolution/action-contract.next.udl +45 -0
  7. package/conformance/evolution/product-identity.expected.json +10 -0
  8. package/conformance/evolution/product-identity.live.udl +55 -0
  9. package/conformance/evolution/product-identity.next.udl +55 -0
  10. package/conformance/evolution/version-required.expected.json +10 -0
  11. package/conformance/evolution/version-required.live.udl +55 -0
  12. package/conformance/evolution/version-required.next.udl +58 -0
  13. package/conformance/invalid/action-without-transition.expected.json +10 -0
  14. package/conformance/invalid/{verb-without-transition.udl → action-without-transition.udl} +7 -2
  15. package/conformance/invalid/agent-description-too-long.expected.json +10 -0
  16. package/conformance/invalid/agent-description-too-long.udl +57 -0
  17. package/conformance/invalid/blank-title.expected.json +1 -1
  18. package/conformance/invalid/blank-title.udl +6 -2
  19. package/conformance/invalid/composition-dial-duplicate-key.expected.json +10 -0
  20. package/conformance/invalid/composition-dial-duplicate-key.udl +73 -0
  21. package/conformance/invalid/depth-budget.expected.json +1 -1
  22. package/conformance/invalid/duplicate-subject.expected.json +10 -0
  23. package/conformance/invalid/duplicate-subject.udl +230 -0
  24. package/conformance/invalid/forged-effects.expected.json +10 -0
  25. package/conformance/invalid/forged-effects.udl +76 -0
  26. package/conformance/invalid/format-version.expected.json +1 -1
  27. package/conformance/invalid/format-version.udl +6 -2
  28. package/conformance/invalid/instrument-id-not-snake-case.expected.json +10 -0
  29. package/conformance/invalid/{noun-id-not-snake-case.udl → instrument-id-not-snake-case.udl} +6 -2
  30. package/conformance/invalid/invalid-aggregate-gate-shape.expected.json +10 -0
  31. package/conformance/invalid/invalid-aggregate-gate-shape.udl +1819 -0
  32. package/conformance/invalid/invalid-check-duration.expected.json +10 -0
  33. package/conformance/invalid/invalid-check-duration.udl +219 -0
  34. package/conformance/invalid/invalid-dial-anchor.expected.json +10 -0
  35. package/conformance/invalid/invalid-dial-anchor.udl +219 -0
  36. package/conformance/invalid/invalid-exception-parent-ref.expected.json +10 -0
  37. package/conformance/invalid/invalid-exception-parent-ref.udl +2487 -0
  38. package/conformance/invalid/invalid-exposure-shape.expected.json +10 -0
  39. package/conformance/invalid/invalid-exposure-shape.udl +1819 -0
  40. package/conformance/invalid/invalid-remainder.expected.json +10 -0
  41. package/conformance/invalid/invalid-remainder.udl +220 -0
  42. package/conformance/invalid/invalid-schema-keyword.expected.json +10 -0
  43. package/conformance/invalid/invalid-schema-keyword.udl +220 -0
  44. package/conformance/invalid/invalid-utf8.expected.json +1 -1
  45. package/conformance/invalid/malformed-json.expected.json +1 -1
  46. package/conformance/invalid/missing-create-action.expected.json +10 -0
  47. package/conformance/invalid/{missing-create-verb.udl → missing-create-action.udl} +5 -2
  48. package/conformance/invalid/missing-exception-amount-field.expected.json +10 -0
  49. package/conformance/invalid/missing-exception-amount-field.udl +2487 -0
  50. package/conformance/invalid/missing-exception-contract.expected.json +14 -0
  51. package/conformance/invalid/missing-exception-contract.udl +2523 -0
  52. package/conformance/invalid/missing-exception-reason-field.expected.json +10 -0
  53. package/conformance/invalid/missing-exception-reason-field.udl +2487 -0
  54. package/conformance/invalid/not-an-object.expected.json +1 -1
  55. package/conformance/invalid/payout-reconcile-not-a-bank-debit.expected.json +10 -0
  56. package/conformance/invalid/payout-reconcile-not-a-bank-debit.udl +259 -0
  57. package/conformance/invalid/quote-freeze-set-incomplete.expected.json +10 -0
  58. package/conformance/invalid/quote-freeze-set-incomplete.udl +261 -0
  59. package/conformance/invalid/quote-named-reference-gate.expected.json +10 -0
  60. package/conformance/invalid/quote-named-reference-gate.udl +50 -0
  61. package/conformance/invalid/reconcile-named-reference-gate.expected.json +10 -0
  62. package/conformance/invalid/reconcile-named-reference-gate.udl +50 -0
  63. package/conformance/invalid/unknown-key.expected.json +1 -1
  64. package/conformance/invalid/unknown-key.udl +6 -2
  65. package/conformance/invalid/unknown-reference-gate-field.expected.json +10 -0
  66. package/conformance/invalid/unknown-reference-gate-field.udl +1819 -0
  67. package/conformance/invalid/unknown-required-field.expected.json +10 -0
  68. package/conformance/invalid/unknown-required-field.udl +220 -0
  69. package/conformance/invalid/unreachable-state.expected.json +2 -2
  70. package/conformance/invalid/unreachable-state.udl +6 -2
  71. package/conformance/invalid/wrong-exception-amount-field.expected.json +10 -0
  72. package/conformance/invalid/wrong-exception-amount-field.udl +2487 -0
  73. package/conformance/invalid/wrong-exception-reason-field.expected.json +10 -0
  74. package/conformance/invalid/wrong-exception-reason-field.udl +2487 -0
  75. package/conformance/valid/agent-description.expected.json +6 -0
  76. package/conformance/valid/agent-description.udl +66 -0
  77. package/conformance/valid/cards.expected.json +1 -0
  78. package/conformance/valid/cards.udl +843 -356
  79. package/conformance/valid/commerce-escrow.expected.json +1 -0
  80. package/conformance/valid/commerce-escrow.udl +792 -291
  81. package/conformance/valid/compiled-crowdfunding.expected.json +6 -0
  82. package/conformance/valid/compiled-crowdfunding.udl +1843 -0
  83. package/conformance/valid/compiled-watch-club.expected.json +6 -0
  84. package/conformance/valid/compiled-watch-club.udl +2486 -0
  85. package/conformance/valid/complete-contract.expected.json +6 -0
  86. package/conformance/valid/complete-contract.udl +218 -0
  87. package/conformance/valid/effect-signatures.expected.json +6 -0
  88. package/conformance/valid/effect-signatures.udl +75 -0
  89. package/conformance/valid/hand-edited.expected.json +1 -0
  90. package/conformance/valid/hand-edited.udl +1 -1
  91. package/conformance/valid/insured-car-marketplace.expected.json +1 -0
  92. package/conformance/valid/insured-car-marketplace.udl +534 -315
  93. package/conformance/valid/insured-travel.expected.json +2 -1
  94. package/conformance/valid/insured-travel.udl +1489 -641
  95. package/conformance/valid/minimal.expected.json +1 -0
  96. package/conformance/valid/minimal.udl +18 -14
  97. package/conformance/valid/protection.expected.json +1 -0
  98. package/conformance/valid/protection.udl +719 -304
  99. package/conformance/valid/string-escaping.expected.json +6 -0
  100. package/conformance/valid/string-escaping.udl +54 -0
  101. package/dist/canonical.d.ts +2 -0
  102. package/dist/canonical.d.ts.map +1 -1
  103. package/dist/canonical.js +7 -0
  104. package/dist/canonical.js.map +1 -1
  105. package/dist/check-profiles.d.ts +57 -0
  106. package/dist/check-profiles.d.ts.map +1 -0
  107. package/dist/check-profiles.js +62 -0
  108. package/dist/check-profiles.js.map +1 -0
  109. package/dist/cli.js +48 -4
  110. package/dist/cli.js.map +1 -1
  111. package/dist/diagnostics.d.ts +168 -0
  112. package/dist/diagnostics.d.ts.map +1 -0
  113. package/dist/diagnostics.js +164 -0
  114. package/dist/diagnostics.js.map +1 -0
  115. package/dist/effects.d.ts +41 -0
  116. package/dist/effects.d.ts.map +1 -0
  117. package/dist/effects.js +110 -0
  118. package/dist/effects.js.map +1 -0
  119. package/dist/evolution.d.ts +51 -18
  120. package/dist/evolution.d.ts.map +1 -1
  121. package/dist/evolution.js +244 -102
  122. package/dist/evolution.js.map +1 -1
  123. package/dist/finance.d.ts +17 -13
  124. package/dist/finance.d.ts.map +1 -1
  125. package/dist/finance.js +165 -109
  126. package/dist/finance.js.map +1 -1
  127. package/dist/index.d.ts +12 -8
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +8 -5
  130. package/dist/index.js.map +1 -1
  131. package/dist/limits.d.ts +2 -2
  132. package/dist/limits.d.ts.map +1 -1
  133. package/dist/limits.js +4 -3
  134. package/dist/limits.js.map +1 -1
  135. package/dist/parser.d.ts.map +1 -1
  136. package/dist/parser.js +6 -16
  137. package/dist/parser.js.map +1 -1
  138. package/dist/schema.d.ts +2059 -135
  139. package/dist/schema.d.ts.map +1 -1
  140. package/dist/schema.js +759 -73
  141. package/dist/schema.js.map +1 -1
  142. package/dist/validation.d.ts +22 -9
  143. package/dist/validation.d.ts.map +1 -1
  144. package/dist/validation.js +1463 -457
  145. package/dist/validation.js.map +1 -1
  146. package/docs/README.md +14 -0
  147. package/docs/guide/01-a-document.md +37 -0
  148. package/docs/guide/02-money-steps.md +23 -0
  149. package/docs/guide/03-laws.md +18 -0
  150. package/docs/guide/04-fees-and-remainder.md +15 -0
  151. package/docs/guide/05-checks-updates-dials.md +7 -0
  152. package/docs/guide/06-effects.md +11 -0
  153. package/docs/guide/07-evolution.md +11 -0
  154. package/docs/guide/08-implementing.md +13 -0
  155. package/docs/llms-full.txt +1241 -0
  156. package/docs/llms.txt +14 -0
  157. package/docs/reference/canonical.md +16 -0
  158. package/docs/reference/clauses.md +986 -0
  159. package/docs/reference/cli.md +22 -0
  160. package/docs/reference/diagnostics.md +32 -0
  161. package/package.json +6 -2
  162. package/skills/udl/SKILL.md +28 -0
  163. package/spec/README.md +82 -51
  164. package/spec/udl.schema.json +1191 -119
  165. package/src/canonical.ts +11 -0
  166. package/src/check-profiles.ts +80 -0
  167. package/src/cli.ts +49 -3
  168. package/src/diagnostics.ts +221 -0
  169. package/src/effects.ts +163 -0
  170. package/src/evolution.ts +375 -125
  171. package/src/finance.ts +255 -160
  172. package/src/index.ts +50 -14
  173. package/src/limits.ts +4 -3
  174. package/src/parser.ts +15 -18
  175. package/src/schema.ts +855 -81
  176. package/src/validation.ts +2666 -560
  177. package/conformance/invalid/missing-create-verb.expected.json +0 -10
  178. package/conformance/invalid/noun-id-not-snake-case.expected.json +0 -10
  179. package/conformance/invalid/verb-without-transition.expected.json +0 -10
package/src/evolution.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import { UDL_LIMITS } from "./limits.js";
2
- import type { UdlAggregate, UdlDocument, UdlNoun, UdlVerb } from "./schema.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";
3
10
  import { UdlError } from "./validation.js";
11
+ import { issue, type UdlIssue } from "./diagnostics.js";
4
12
 
5
13
  export interface EvolutionFieldSnapshot {
6
14
  readonly required: boolean;
@@ -17,35 +25,50 @@ export interface EvolutionMoveSnapshot extends EvolutionStepSnapshot {
17
25
  readonly key: string;
18
26
  }
19
27
 
20
- export interface EvolutionVerbSnapshot {
28
+ export interface EvolutionActionSnapshot {
21
29
  /** Missing on snapshots written before receipt-input capture existed. */
22
30
  readonly captureInput?: unknown;
23
31
  readonly deadline: unknown;
24
32
  readonly decision: unknown;
33
+ /** Missing on snapshots written before UDL completeness. */
34
+ readonly principal?: string | null;
35
+ /** Missing on snapshots written before quote-commit existed. */
36
+ readonly commit?: string | null;
37
+ /** Missing on snapshots written before UDL completeness. */
38
+ readonly remainder?: unknown;
39
+ /** Missing on snapshots written before UDL completeness. */
40
+ readonly requiresChecks?: unknown;
41
+ /** Missing on snapshots written before UDL completeness. */
42
+ readonly sandboxFailurePoint?: string | null;
43
+ /** Missing on snapshots written before UDL completeness. */
44
+ readonly updates?: readonly string[];
25
45
  /** Missing on snapshots written before weighted distribution existed. */
26
46
  readonly distribute?: unknown;
27
47
  readonly due: unknown;
28
48
  readonly earnable: boolean;
49
+ /** Missing on snapshots written before derived effect rows entered UDL. */
50
+ readonly effects?: unknown;
29
51
  readonly eventName: string | null;
30
52
  readonly input: Readonly<Record<string, EvolutionFieldSnapshot>> | null;
31
53
  /**
32
54
  * Every top-level input schema keyword EXCEPT properties/required (those are
33
- * diffed field by field). additionalProperties, oneOf, minProperties, and
34
- * friends change what callers may send, so they are frozen once live.
55
+ * diffed field by field). additionalProperties, string and array bounds,
56
+ * numeric bounds, enum, const, pattern, and format change what callers may
57
+ * send, so they are frozen once live.
35
58
  */
36
59
  readonly inputConstraints: unknown;
37
60
  /** Missing on snapshots written before payout intents existed. */
38
61
  readonly payout?: unknown;
39
62
  readonly port: unknown;
40
- /** Missing on snapshots written before public intent metadata existed. */
41
- readonly publicIntent?: string | null;
63
+ /** Missing on snapshots written before public action metadata existed. */
64
+ readonly publicAction?: string | null;
42
65
  readonly requiresAggregate: unknown;
43
66
  readonly requiresDrainedAccount: unknown;
44
67
  /** Missing on snapshots written before exposure gates entered open UDL. */
45
68
  readonly requiresExposure?: unknown;
46
69
  readonly requiresRefs: unknown;
47
- /** Missing on snapshots written before settlement evidence gates existed. */
48
- readonly requiresSettlement?: unknown;
70
+ /** Missing on snapshots written before reconcile expectations existed. */
71
+ readonly reconcile?: unknown;
49
72
  readonly setsAt: unknown;
50
73
  /** Missing on snapshots written before signed child sums existed. */
51
74
  readonly signedSum?: unknown;
@@ -59,69 +82,118 @@ export interface EvolutionTransitionSnapshot {
59
82
  }
60
83
 
61
84
  /** The complete serializable algebra protected by append-only evolution. */
62
- export interface NounEvolutionSnapshot {
85
+ export interface InstrumentEvolutionSnapshot {
86
+ /** Missing on snapshots written before UDL carried authored action order. */
87
+ readonly actionOrder?: readonly string[];
63
88
  readonly aggregateInvariants: readonly string[];
64
- /** Missing on snapshots written before typed computed refs entered UDL. */
65
- readonly computedMoneyRefs?: readonly string[];
89
+ /** Missing on snapshots written before UDL completeness. */
90
+ readonly callerParkedStates?: Readonly<Record<string, string>>;
66
91
  readonly derivedAmounts: readonly string[];
92
+ /** Missing on snapshots written before UDL completeness. */
93
+ readonly dials?: unknown;
67
94
  readonly distinctParties?: true;
95
+ /** Missing on snapshots written before unified fee rules entered UDL. */
96
+ readonly feeRules?: unknown;
68
97
  readonly fields: Readonly<Record<string, EvolutionFieldSnapshot>>;
69
98
  readonly id: string;
70
99
  readonly idPrefix: string;
71
100
  readonly initial: string;
101
+ /** Missing on snapshots written before UDL completeness. */
102
+ readonly nav?: readonly string[];
72
103
  readonly parties: Readonly<Record<string, string>>;
73
104
  readonly partitions: readonly string[];
74
105
  readonly states: readonly string[];
75
106
  readonly subjects: readonly string[];
107
+ /** Missing on snapshots written before UDL completeness. */
108
+ readonly subjectExtensible?: boolean;
109
+ /** Missing on snapshots written before UDL completeness. */
110
+ readonly surfaceVisibility?: string | null;
111
+ /** Missing on snapshots written before UDL completeness. */
112
+ readonly templateId?: string | null;
76
113
  readonly transitions: Readonly<Record<string, EvolutionTransitionSnapshot>>;
77
- readonly unwind: unknown;
114
+ /** Every quote-commit pair, keyed by quoting action. */
115
+ readonly quotes: unknown;
78
116
  readonly updateFields: readonly string[];
117
+ /** Missing on snapshots written before UDL completeness. */
118
+ readonly updateExamples?: unknown;
79
119
  readonly updateStates: readonly string[];
80
- readonly verbs: Readonly<Record<string, EvolutionVerbSnapshot>>;
120
+ readonly actions: Readonly<Record<string, EvolutionActionSnapshot>>;
81
121
  }
82
122
 
83
- /** Project an open .udl noun into the evolution engine's exact semantic shape. */
84
- export function snapshotUdlNoun(noun: UdlNoun): NounEvolutionSnapshot {
85
- const required = new Set(noun.required);
123
+ /** Project an open .udl instrument into the evolution engine's exact semantic shape. */
124
+ export function snapshotUdlInstrument(
125
+ instrument: UdlInstrument,
126
+ ): InstrumentEvolutionSnapshot {
127
+ const required = new Set(instrument.required);
86
128
  return {
87
- aggregateInvariants: (noun.aggregateInvariants ?? []).map(
129
+ actionOrder: [...instrument.actionOrder],
130
+ aggregateInvariants: (instrument.aggregateInvariants ?? []).map(
88
131
  aggregateInvariantKey,
89
132
  ),
90
- computedMoneyRefs: [...(noun.computedMoneyRefs ?? [])].sort(),
91
- derivedAmounts: (noun.derivedAmounts ?? []).map(
133
+ callerParkedStates: instrument.callerParkedStates ?? {},
134
+ derivedAmounts: (instrument.derivedAmounts ?? []).map(
92
135
  (amount) =>
93
136
  `${amount.field}=floor(${amount.sourceField}*${amount.rule.bps}/10000)`,
94
137
  ),
95
- ...(noun.distinctParties ? { distinctParties: true as const } : {}),
138
+ dials: instrument.dials ?? [],
139
+ ...(instrument.distinctParties ? { distinctParties: true as const } : {}),
140
+ ...(instrument.feeRules
141
+ ? {
142
+ feeRules: instrument.feeRules.map((fee) => ({
143
+ ...fee,
144
+ rule:
145
+ fee.rule.kind === "tiered"
146
+ ? {
147
+ ...fee.rule,
148
+ tiers: fee.rule.tiers.map((tier) => ({
149
+ ...tier,
150
+ rule: { ...tier.rule },
151
+ })),
152
+ }
153
+ : { ...fee.rule },
154
+ })),
155
+ }
156
+ : {}),
96
157
  fields: Object.fromEntries(
97
- Object.entries(noun.fields).map(([field, schema]) => [
158
+ Object.entries(instrument.fields).map(([field, schema]) => [
98
159
  field,
99
160
  { required: required.has(field), schema },
100
161
  ]),
101
162
  ),
102
- id: noun.id,
103
- idPrefix: noun.idPrefix,
104
- initial: noun.lifecycle.initial,
105
- parties: compactStringRecord(noun.parties),
106
- partitions: (noun.partitions ?? []).map(
163
+ id: instrument.id,
164
+ idPrefix: instrument.idPrefix,
165
+ initial: instrument.lifecycle.initial,
166
+ nav: instrument.nav ?? [],
167
+ parties: compactStringRecord(instrument.parties),
168
+ partitions: (instrument.partitions ?? []).map(
107
169
  (partition) =>
108
170
  `${partition.totalField} = ${[...partition.pieceFields].sort().join(" + ")}`,
109
171
  ),
110
- states: [...noun.lifecycle.states],
111
- subjects: noun.subject ? [...noun.subject.kinds] : [],
172
+ states: [...instrument.lifecycle.states],
173
+ subjects: instrument.subject ? [...instrument.subject.kinds] : [],
174
+ subjectExtensible: instrument.subject?.extensible ?? false,
175
+ surfaceVisibility: instrument.surfaceVisibility ?? null,
176
+ templateId: instrument.templateId ?? null,
112
177
  transitions: Object.fromEntries(
113
- Object.entries(noun.lifecycle.transitions).map(([verb, transition]) => [
114
- verb,
115
- { from: [...transition.from], to: transition.to },
116
- ]),
178
+ Object.entries(instrument.lifecycle.transitions).map(
179
+ ([action, transition]) => [
180
+ action,
181
+ { from: [...transition.from], to: transition.to },
182
+ ],
183
+ ),
184
+ ),
185
+ quotes: Object.fromEntries(
186
+ Object.entries(instrument.actions)
187
+ .filter(([, action]) => action.quote)
188
+ .map(([name, action]) => [name, action.quote]),
117
189
  ),
118
- unwind: noun.unwind ?? null,
119
- updateFields: noun.update?.fields ?? [],
120
- updateStates: noun.update?.states ?? [],
121
- verbs: Object.fromEntries(
122
- Object.entries(noun.verbs).map(([verb, definition]) => [
123
- verb,
124
- snapshotUdlVerb(definition),
190
+ updateFields: instrument.update?.fields ?? [],
191
+ updateExamples: instrument.update?.examples ?? null,
192
+ updateStates: instrument.update?.states ?? [],
193
+ actions: Object.fromEntries(
194
+ instrument.actionOrder.map((action) => [
195
+ action,
196
+ snapshotUdlAction(instrument.actions[action]!),
125
197
  ]),
126
198
  ),
127
199
  };
@@ -138,6 +210,23 @@ export function snapshotUdlNoun(noun: UdlNoun): NounEvolutionSnapshot {
138
210
  export function diffValidatedUdlEvolution(
139
211
  previous: UdlDocument,
140
212
  next: UdlDocument,
213
+ ): readonly UdlIssue[] {
214
+ return diffValidatedUdlEvolutionMessages(previous, next).map((message) => {
215
+ const instrumentId = /^([^:]+): /.exec(message)?.[1];
216
+ const instrumentIndex = previous.instruments.findIndex(
217
+ (instrument) => instrument.id === instrumentId,
218
+ );
219
+ const base =
220
+ instrumentIndex < 0
221
+ ? "$.instruments"
222
+ : `$.instruments[${instrumentIndex}]`;
223
+ return evolutionIssue(message, base);
224
+ });
225
+ }
226
+
227
+ function diffValidatedUdlEvolutionMessages(
228
+ previous: UdlDocument,
229
+ next: UdlDocument,
141
230
  ): readonly string[] {
142
231
  const violations: string[] = [];
143
232
  if (previous.product !== next.product) {
@@ -172,26 +261,32 @@ export function diffValidatedUdlEvolution(
172
261
  if (!previousSubjects.has(kind)) semanticChange = true;
173
262
  }
174
263
 
175
- const previousNouns = new Map(
176
- previous.nouns.map((noun) => [noun.id, snapshotUdlNoun(noun)] as const),
264
+ const previousInstruments = new Map(
265
+ previous.instruments.map(
266
+ (instrument) =>
267
+ [instrument.id, snapshotUdlInstrument(instrument)] as const,
268
+ ),
177
269
  );
178
- const nextNouns = new Map(
179
- next.nouns.map((noun) => [noun.id, snapshotUdlNoun(noun)] as const),
270
+ const nextInstruments = new Map(
271
+ next.instruments.map(
272
+ (instrument) =>
273
+ [instrument.id, snapshotUdlInstrument(instrument)] as const,
274
+ ),
180
275
  );
181
- for (const [id, noun] of previousNouns) {
182
- const current = nextNouns.get(id);
276
+ for (const [id, instrument] of previousInstruments) {
277
+ const current = nextInstruments.get(id);
183
278
  if (!current) {
184
279
  semanticChange = true;
185
- violations.push(`${id}: live noun was removed from the product`);
280
+ violations.push(`${id}: live instrument was removed from the product`);
186
281
  continue;
187
282
  }
188
- if (stableStringify(noun) !== stableStringify(current)) {
283
+ if (stableStringify(instrument) !== stableStringify(current)) {
189
284
  semanticChange = true;
190
285
  }
191
- violations.push(...diffNounEvolution(noun, current));
286
+ violations.push(...diffInstrumentEvolutionMessages(instrument, current));
192
287
  }
193
- for (const id of nextNouns.keys()) {
194
- if (!previousNouns.has(id)) semanticChange = true;
288
+ for (const id of nextInstruments.keys()) {
289
+ if (!previousInstruments.has(id)) semanticChange = true;
195
290
  }
196
291
 
197
292
  if (semanticChange && next.version <= previous.version) {
@@ -202,13 +297,22 @@ export function diffValidatedUdlEvolution(
202
297
  return violations;
203
298
  }
204
299
 
205
- /** Diff one noun snapshot. An empty result means the change is additive. */
206
- export function diffNounEvolution(
207
- previous: NounEvolutionSnapshot,
208
- next: NounEvolutionSnapshot,
300
+ /** Diff one instrument snapshot. An empty result means the change is additive. */
301
+ export function diffInstrumentEvolution(
302
+ previous: InstrumentEvolutionSnapshot,
303
+ next: InstrumentEvolutionSnapshot,
304
+ ): readonly UdlIssue[] {
305
+ return diffInstrumentEvolutionMessages(previous, next).map((message) =>
306
+ evolutionIssue(message, "$.instruments"),
307
+ );
308
+ }
309
+
310
+ function diffInstrumentEvolutionMessages(
311
+ previous: InstrumentEvolutionSnapshot,
312
+ next: InstrumentEvolutionSnapshot,
209
313
  ): readonly string[] {
210
314
  if (previous.id !== next.id) {
211
- return [`noun id changed from ${previous.id} to ${next.id}`];
315
+ return [`instrument id changed from ${previous.id} to ${next.id}`];
212
316
  }
213
317
  const violations: string[] = [];
214
318
  if (previous.idPrefix !== next.idPrefix) {
@@ -229,15 +333,15 @@ export function diffNounEvolution(
229
333
  (state) => `lifecycle state ${state} was removed or renamed`,
230
334
  ),
231
335
  );
232
- for (const [verb, transition] of Object.entries(previous.transitions)) {
233
- const current = next.transitions[verb];
336
+ for (const [action, transition] of Object.entries(previous.transitions)) {
337
+ const current = next.transitions[action];
234
338
  if (!current) {
235
- violations.push(`transition for verb ${verb} was removed or renamed`);
339
+ violations.push(`transition for action ${action} was removed or renamed`);
236
340
  continue;
237
341
  }
238
342
  if (transition.to !== current.to) {
239
343
  violations.push(
240
- `transition for verb ${verb} changed its target state from ${transition.to} to ${current.to}`,
344
+ `transition for action ${action} changed its target state from ${transition.to} to ${current.to}`,
241
345
  );
242
346
  }
243
347
  violations.push(
@@ -245,7 +349,7 @@ export function diffNounEvolution(
245
349
  transition.from,
246
350
  current.from,
247
351
  (state) =>
248
- `transition for verb ${verb} no longer fires from state ${state}`,
352
+ `transition for action ${action} no longer fires from state ${state}`,
249
353
  ),
250
354
  );
251
355
  }
@@ -254,17 +358,47 @@ export function diffNounEvolution(
254
358
  if (Boolean(previous.distinctParties) !== Boolean(next.distinctParties)) {
255
359
  violations.push("distinct-party admission changed after becoming live");
256
360
  }
257
- violations.push(...diffVerbs(previous.verbs, next.verbs));
361
+ if (previous.actionOrder) {
362
+ const previousActions = new Set(previous.actionOrder);
363
+ const retainedActionOrder = (next.actionOrder ?? []).filter((action) =>
364
+ previousActions.has(action),
365
+ );
366
+ if (
367
+ stableStringify(previous.actionOrder) !==
368
+ stableStringify(retainedActionOrder)
369
+ ) {
370
+ violations.push("instrument action order changed after becoming live");
371
+ }
372
+ }
373
+ violations.push(...diffActions(previous.actions, next.actions));
258
374
  violations.push(...diffParties(previous.parties, next.parties));
259
375
  violations.push(...diffAggregates(previous, next));
260
376
  if (
261
- stableStringify(previous.computedMoneyRefs ?? []) !==
262
- stableStringify(next.computedMoneyRefs ?? [])
377
+ stableStringify(Object.keys(previous.callerParkedStates ?? {}).sort()) !==
378
+ stableStringify(Object.keys(next.callerParkedStates ?? {}).sort())
379
+ ) {
380
+ violations.push("caller-parked state annotations changed");
381
+ }
382
+ if (
383
+ stableStringify(previous.dials ?? []) !== stableStringify(next.dials ?? [])
384
+ ) {
385
+ violations.push("instrument dials changed after becoming live");
386
+ }
387
+ if (
388
+ (previous.surfaceVisibility ?? null) !== (next.surfaceVisibility ?? null)
263
389
  ) {
264
390
  violations.push(
265
- "computed money refs changed; runtime-derived money surfaces are frozen once live",
391
+ "instrument surface visibility changed after becoming live",
266
392
  );
267
393
  }
394
+ if ((previous.templateId ?? null) !== (next.templateId ?? null)) {
395
+ violations.push(
396
+ "instrument archetype template changed after becoming live",
397
+ );
398
+ }
399
+ if (previous.subjectExtensible && !next.subjectExtensible) {
400
+ violations.push("tenant-defined subject kinds are no longer accepted");
401
+ }
268
402
  if (
269
403
  stableStringify(previous.derivedAmounts ?? []) !==
270
404
  stableStringify(next.derivedAmounts ?? [])
@@ -273,6 +407,14 @@ export function diffNounEvolution(
273
407
  "derived amount rules changed; derived money arithmetic is frozen once live",
274
408
  );
275
409
  }
410
+ if (
411
+ stableStringify(previous.feeRules ?? []) !==
412
+ stableStringify(next.feeRules ?? [])
413
+ ) {
414
+ violations.push(
415
+ "feeRules changed; fee calculation and settlement funding are frozen once live",
416
+ );
417
+ }
276
418
  violations.push(
277
419
  ...removedSetEntries(
278
420
  previous.partitions,
@@ -286,7 +428,7 @@ export function diffNounEvolution(
286
428
  `partition law [${law}] was added, which can reject existing callers`,
287
429
  ),
288
430
  );
289
- violations.push(...diffUnwind(previous.unwind, next.unwind));
431
+ violations.push(...diffQuotes(previous.quotes, next.quotes));
290
432
  violations.push(
291
433
  ...removedSetEntries(
292
434
  previous.subjects,
@@ -307,14 +449,89 @@ export function diffNounEvolution(
307
449
  return violations.map((violation) => `${next.id}: ${violation}`);
308
450
  }
309
451
 
310
- function snapshotUdlVerb(definition: UdlVerb): EvolutionVerbSnapshot {
452
+ function evolutionIssue(message: string, instrumentBase: string): UdlIssue {
453
+ if (message.startsWith("product id changed"))
454
+ return issue("UDL7001", "$.product", message);
455
+ if (message.startsWith("product version moved backward"))
456
+ return issue("UDL7001", "$.version", message);
457
+ if (message.startsWith("product definition changed"))
458
+ return issue("UDL7002", "$.version", message);
459
+ if (message.startsWith("subject kind "))
460
+ return issue("UDL7001", "$.subjects", message);
461
+
462
+ const instrumentMatch = /^([^:]+): (.*)$/.exec(message);
463
+ const detail = instrumentMatch?.[2] ?? message;
464
+ const transitionAction = /^transition for action ([a-z][a-z0-9_]*)/.exec(
465
+ detail,
466
+ )?.[1];
467
+ if (transitionAction)
468
+ return issue(
469
+ "UDL7001",
470
+ `${instrumentBase}.lifecycle.transitions.${transitionAction}`,
471
+ message,
472
+ );
473
+ const action = /^action ([a-z][a-z0-9_]*)/.exec(detail)?.[1];
474
+ if (action)
475
+ return issue("UDL7001", `${instrumentBase}.actions.${action}`, message);
476
+ if (detail.includes("instrument id"))
477
+ return issue("UDL7001", instrumentBase, message);
478
+ if (
479
+ detail.includes("id prefix") ||
480
+ detail.includes("lifecycle") ||
481
+ detail.includes("transition") ||
482
+ detail.includes("initial") ||
483
+ detail.includes("live instrument was removed")
484
+ )
485
+ return issue("UDL7001", `${instrumentBase}.lifecycle`, message);
486
+ if (detail.startsWith("field "))
487
+ return issue("UDL7001", `${instrumentBase}.fields`, message);
488
+ if (
489
+ detail.includes("move") ||
490
+ detail.includes("payout") ||
491
+ detail.includes("fee") ||
492
+ detail.includes("quote") ||
493
+ detail.includes("derived amount") ||
494
+ detail.includes("partition") ||
495
+ detail.includes("distribution") ||
496
+ detail.includes("signed child sum") ||
497
+ detail.includes("earnable")
498
+ )
499
+ return issue("UDL7001", `${instrumentBase}.actions`, message);
500
+ if (
501
+ detail.includes("gate") ||
502
+ detail.includes("check prerequisite") ||
503
+ detail.includes("reconcile expectation") ||
504
+ detail.includes("drained-account") ||
505
+ detail.includes("exposure") ||
506
+ detail.includes("aggregate")
507
+ )
508
+ return issue("UDL7001", `${instrumentBase}.actions`, message);
509
+ if (
510
+ detail.startsWith("action ") ||
511
+ detail.includes("action order") ||
512
+ detail.includes("step count")
513
+ )
514
+ return issue("UDL7001", `${instrumentBase}.actions`, message);
515
+ return issue("UDL7001", instrumentBase, message);
516
+ }
517
+
518
+ function snapshotUdlAction(definition: UdlAction): EvolutionActionSnapshot {
311
519
  return {
312
520
  captureInput: definition.captureInput ?? null,
521
+ commit: definition.commit ?? null,
313
522
  deadline: definition.deadline ?? null,
314
523
  decision: definition.decision ?? null,
524
+ principal: definition.principal ?? null,
525
+ remainder: definition.remainder ?? null,
526
+ requiresChecks: definition.requiresChecks ?? [],
527
+ sandboxFailurePoint: definition.sandboxFailurePoint ?? null,
528
+ updates: definition.updates ?? [],
315
529
  distribute: definition.distribute ?? null,
316
530
  due: definition.due ?? null,
317
531
  earnable: definition.earnable ?? false,
532
+ effects:
533
+ definition.effects ??
534
+ deriveUdlActionEffects(definition, udlClauseVocabulary),
318
535
  eventName: definition.eventName ?? null,
319
536
  input: definition.input ? snapshotJsonSchemaFields(definition.input) : null,
320
537
  inputConstraints: definition.input
@@ -322,7 +539,7 @@ function snapshotUdlVerb(definition: UdlVerb): EvolutionVerbSnapshot {
322
539
  : null,
323
540
  payout: definition.payout ?? null,
324
541
  port: definition.port ?? null,
325
- publicIntent: definition.publicIntent ?? null,
542
+ publicAction: definition.publicAction ?? null,
326
543
  setsAt: definition.setsAt ?? null,
327
544
  signedSum: definition.signedSum ?? null,
328
545
  moves: definition.moves.map((move) => ({
@@ -335,7 +552,7 @@ function snapshotUdlVerb(definition: UdlVerb): EvolutionVerbSnapshot {
335
552
  requiresDrainedAccount: definition.requiresDrainedAccount ?? null,
336
553
  requiresExposure: definition.requiresExposure ?? [],
337
554
  requiresRefs: definition.requiresRefs ?? [],
338
- requiresSettlement: definition.requiresSettlement ?? null,
555
+ reconcile: definition.reconcile ?? null,
339
556
  steps: definition.steps.map((step) => ({
340
557
  bind: step.bind,
341
558
  capture: step.capture ?? {},
@@ -406,48 +623,48 @@ function diffFields(
406
623
  return violations;
407
624
  }
408
625
 
409
- function diffVerbs(
410
- previous: Readonly<Record<string, EvolutionVerbSnapshot>>,
411
- next: Readonly<Record<string, EvolutionVerbSnapshot>>,
626
+ function diffActions(
627
+ previous: Readonly<Record<string, EvolutionActionSnapshot>>,
628
+ next: Readonly<Record<string, EvolutionActionSnapshot>>,
412
629
  ): string[] {
413
630
  const violations: string[] = [];
414
- for (const [verb, descriptor] of Object.entries(previous)) {
415
- const current = next[verb];
631
+ for (const [action, descriptor] of Object.entries(previous)) {
632
+ const current = next[action];
416
633
  if (!current) {
417
- violations.push(`verb ${verb} was removed or renamed`);
634
+ violations.push(`action ${action} was removed or renamed`);
418
635
  continue;
419
636
  }
420
637
  if (descriptor.moves.length !== current.moves.length) {
421
638
  violations.push(
422
- `verb ${verb} changed its move count from ${descriptor.moves.length} to ${current.moves.length}; money movement is frozen once live`,
639
+ `action ${action} changed its move count from ${descriptor.moves.length} to ${current.moves.length}; money movement is frozen once live`,
423
640
  );
424
641
  } else {
425
642
  descriptor.moves.forEach((move, index) => {
426
643
  const nextMove = current.moves[index] as EvolutionMoveSnapshot;
427
644
  if (stableStringify(move) !== stableStringify(nextMove)) {
428
645
  violations.push(
429
- `verb ${verb} changed move ${move.key} at index ${index}; money movement is frozen once live`,
646
+ `action ${action} changed move ${move.key} at index ${index}; money movement is frozen once live`,
430
647
  );
431
648
  }
432
649
  });
433
650
  }
434
651
  if (descriptor.steps.length !== current.steps.length) {
435
652
  violations.push(
436
- `verb ${verb} changed its step count from ${descriptor.steps.length} to ${current.steps.length}; a live verb's steps are frozen`,
653
+ `action ${action} changed its step count from ${descriptor.steps.length} to ${current.steps.length}; a live action's steps are frozen`,
437
654
  );
438
655
  } else {
439
656
  descriptor.steps.forEach((step, index) => {
440
657
  const nextStep = current.steps[index] as EvolutionStepSnapshot;
441
658
  if (stableStringify(step) === stableStringify(nextStep)) return;
442
659
  violations.push(
443
- `verb ${verb} changed step ${index} (${step.operation}); a live verb's steps are frozen`,
660
+ `action ${action} changed step ${index} (${step.operation}); a live action's steps are frozen`,
444
661
  );
445
662
  });
446
663
  }
447
664
  violations.push(
448
- ...diffInput(verb, descriptor.input ?? {}, current.input ?? {}),
665
+ ...diffInput(action, descriptor.input ?? {}, current.input ?? {}),
449
666
  );
450
- // A verb gaining its first input is additive (per-field checks still catch
667
+ // A action gaining its first input is additive (per-field checks still catch
451
668
  // added-as-required); once an input is live, its schema envelope is frozen.
452
669
  if (
453
670
  descriptor.input !== null &&
@@ -455,72 +672,105 @@ function diffVerbs(
455
672
  stableStringify(current.inputConstraints)
456
673
  ) {
457
674
  violations.push(
458
- `verb ${verb} changed its input schema beyond declared fields; a live verb input is frozen`,
675
+ `action ${action} changed its input schema beyond declared fields; a live action input is frozen`,
459
676
  );
460
677
  }
461
678
  if (
462
679
  stableStringify(descriptor.requiresRefs) !==
463
680
  stableStringify(current.requiresRefs)
464
681
  ) {
465
- violations.push(`verb ${verb} changed its cross-noun gates`);
682
+ violations.push(`action ${action} changed its cross-instrument gates`);
466
683
  }
467
684
  if (
468
685
  stableStringify(descriptor.requiresAggregate) !==
469
686
  stableStringify(current.requiresAggregate)
470
687
  ) {
471
- violations.push(`verb ${verb} changed its aggregate admission gates`);
688
+ violations.push(`action ${action} changed its aggregate admission gates`);
472
689
  }
473
690
  if (
474
691
  stableStringify(descriptor.requiresDrainedAccount) !==
475
692
  stableStringify(current.requiresDrainedAccount)
476
693
  ) {
477
- violations.push(`verb ${verb} changed its drained-account gate`);
694
+ violations.push(`action ${action} changed its drained-account gate`);
478
695
  }
479
696
  if (
480
697
  descriptor.requiresExposure !== undefined &&
481
698
  stableStringify(descriptor.requiresExposure) !==
482
699
  stableStringify(current.requiresExposure)
483
700
  ) {
484
- violations.push(`verb ${verb} changed its exposure admission gates`);
701
+ violations.push(`action ${action} changed its exposure admission gates`);
485
702
  }
486
703
  if (
487
- descriptor.requiresSettlement !== undefined &&
488
- stableStringify(descriptor.requiresSettlement) !==
489
- stableStringify(current.requiresSettlement)
704
+ descriptor.reconcile !== undefined &&
705
+ stableStringify(descriptor.reconcile) !==
706
+ stableStringify(current.reconcile)
490
707
  ) {
491
- violations.push(`verb ${verb} changed its settlement evidence gate`);
708
+ violations.push(`action ${action} changed its reconcile expectations`);
492
709
  }
493
710
  if (
494
711
  stableStringify(descriptor.payout ?? null) !==
495
712
  stableStringify(current.payout ?? null)
496
713
  ) {
497
- violations.push(`verb ${verb} changed its payout intent`);
714
+ violations.push(`action ${action} changed its payout intent`);
498
715
  }
499
716
  if (descriptor.earnable !== current.earnable) {
500
- violations.push(`verb ${verb} changed its earnable flag`);
717
+ violations.push(`action ${action} changed its earnable flag`);
718
+ }
719
+ if (
720
+ descriptor.effects !== undefined &&
721
+ stableStringify(descriptor.effects) !== stableStringify(current.effects)
722
+ ) {
723
+ violations.push(`action ${action} changed its derived effects`);
501
724
  }
502
725
  if (descriptor.eventName !== current.eventName) {
503
- violations.push(`verb ${verb} changed its event name`);
726
+ violations.push(`action ${action} changed its event name`);
504
727
  }
505
728
  if (stableStringify(descriptor.due) !== stableStringify(current.due)) {
506
- violations.push(`verb ${verb} changed its due condition`);
729
+ violations.push(`action ${action} changed its due condition`);
507
730
  }
508
731
  if (
509
732
  stableStringify(descriptor.deadline) !== stableStringify(current.deadline)
510
733
  ) {
511
- violations.push(`verb ${verb} changed its admission deadline`);
734
+ violations.push(`action ${action} changed its admission deadline`);
512
735
  }
513
736
  if (
514
737
  stableStringify(descriptor.decision) !== stableStringify(current.decision)
515
738
  ) {
516
- violations.push(`verb ${verb} changed its provider decision`);
739
+ violations.push(`action ${action} changed its provider decision`);
740
+ }
741
+ if ((descriptor.principal ?? null) !== (current.principal ?? null)) {
742
+ violations.push(`action ${action} changed its caller principal`);
743
+ }
744
+ if (
745
+ stableStringify(descriptor.remainder ?? null) !==
746
+ stableStringify(current.remainder ?? null)
747
+ ) {
748
+ violations.push(`action ${action} changed its remainder rule`);
749
+ }
750
+ if (
751
+ stableStringify(descriptor.requiresChecks ?? []) !==
752
+ stableStringify(current.requiresChecks ?? [])
753
+ ) {
754
+ violations.push(`action ${action} changed its check prerequisites`);
755
+ }
756
+ if (
757
+ (descriptor.sandboxFailurePoint ?? null) !==
758
+ (current.sandboxFailurePoint ?? null)
759
+ ) {
760
+ violations.push(`action ${action} changed its sandbox failure point`);
761
+ }
762
+ if (
763
+ stableStringify(descriptor.updates ?? []) !==
764
+ stableStringify(current.updates ?? [])
765
+ ) {
766
+ violations.push(`action ${action} changed its field updates`);
517
767
  }
518
768
  if (
519
769
  stableStringify(descriptor.distribute) !==
520
770
  stableStringify(current.distribute)
521
771
  ) {
522
772
  violations.push(
523
- `verb ${verb} changed its distribution rule; money distribution is frozen once live`,
773
+ `action ${action} changed its distribution rule; money distribution is frozen once live`,
524
774
  );
525
775
  }
526
776
  if (
@@ -528,41 +778,41 @@ function diffVerbs(
528
778
  stableStringify(descriptor.captureInput) !==
529
779
  stableStringify(current.captureInput)
530
780
  ) {
531
- violations.push(`verb ${verb} changed its captured receipt input`);
781
+ violations.push(`action ${action} changed its captured receipt input`);
532
782
  }
533
783
  if (stableStringify(descriptor.port) !== stableStringify(current.port)) {
534
- violations.push(`verb ${verb} changed its decision port`);
784
+ violations.push(`action ${action} changed its decision port`);
535
785
  }
536
786
  if (
537
- descriptor.publicIntent != null &&
538
- descriptor.publicIntent !== current.publicIntent
787
+ descriptor.publicAction != null &&
788
+ descriptor.publicAction !== current.publicAction
539
789
  ) {
540
- violations.push(`verb ${verb} changed its public intent`);
790
+ violations.push(`action ${action} changed its public action`);
541
791
  }
542
792
  if (
543
793
  stableStringify(descriptor.setsAt) !== stableStringify(current.setsAt)
544
794
  ) {
545
- violations.push(`verb ${verb} changed its computed timestamp`);
795
+ violations.push(`action ${action} changed its computed timestamp`);
546
796
  }
547
797
  if (
548
798
  descriptor.signedSum !== undefined &&
549
799
  stableStringify(descriptor.signedSum) !==
550
800
  stableStringify(current.signedSum)
551
801
  ) {
552
- violations.push(`verb ${verb} changed its signed child sum`);
802
+ violations.push(`action ${action} changed its signed child sum`);
553
803
  }
554
804
  }
555
- for (const [verb, descriptor] of Object.entries(next)) {
556
- if (previous[verb] || descriptor.payout == null) continue;
805
+ for (const [action, descriptor] of Object.entries(next)) {
806
+ if (previous[action] || descriptor.payout == null) continue;
557
807
  violations.push(
558
- `verb ${verb} added a payout intent; external money movement is frozen once live`,
808
+ `action ${action} added a payout intent; external money movement is frozen once live`,
559
809
  );
560
810
  }
561
811
  return violations;
562
812
  }
563
813
 
564
814
  function diffInput(
565
- verb: string,
815
+ action: string,
566
816
  previous: Readonly<Record<string, EvolutionFieldSnapshot>>,
567
817
  next: Readonly<Record<string, EvolutionFieldSnapshot>>,
568
818
  ): string[] {
@@ -570,30 +820,30 @@ function diffInput(
570
820
  for (const [field, descriptor] of Object.entries(previous)) {
571
821
  const current = next[field];
572
822
  if (!current) {
573
- // Compiled verb inputs are strict objects, so a removed field starts
574
- // rejecting every caller that still sends it -- same law as noun fields.
823
+ // Compiled action inputs are strict objects, so a removed field starts
824
+ // rejecting every caller that still sends it -- same law as instrument fields.
575
825
  violations.push(
576
- `verb ${verb} input field ${field} was removed or renamed`,
826
+ `action ${action} input field ${field} was removed or renamed`,
577
827
  );
578
828
  continue;
579
829
  }
580
830
  if (!descriptor.required && current.required) {
581
831
  violations.push(
582
- `verb ${verb} input field ${field} became required, tightening the verb input`,
832
+ `action ${action} input field ${field} became required, tightening the action input`,
583
833
  );
584
834
  }
585
835
  if (
586
836
  stableStringify(descriptor.schema) !== stableStringify(current.schema)
587
837
  ) {
588
838
  violations.push(
589
- `verb ${verb} input field ${field} schema changed; a live verb input schema is frozen`,
839
+ `action ${action} input field ${field} schema changed; a live action input schema is frozen`,
590
840
  );
591
841
  }
592
842
  }
593
843
  for (const [field, descriptor] of Object.entries(next)) {
594
844
  if (!previous[field] && descriptor.required) {
595
845
  violations.push(
596
- `verb ${verb} input field ${field} was added as required, tightening the verb input`,
846
+ `action ${action} input field ${field} was added as required, tightening the action input`,
597
847
  );
598
848
  }
599
849
  }
@@ -618,8 +868,8 @@ function diffParties(
618
868
  }
619
869
 
620
870
  function diffAggregates(
621
- previous: NounEvolutionSnapshot,
622
- next: NounEvolutionSnapshot,
871
+ previous: InstrumentEvolutionSnapshot,
872
+ next: InstrumentEvolutionSnapshot,
623
873
  ): string[] {
624
874
  return [
625
875
  ...removedSetEntries(
@@ -636,13 +886,13 @@ function diffAggregates(
636
886
  ];
637
887
  }
638
888
 
639
- function diffUnwind(previous: unknown, next: unknown): string[] {
640
- if (previous === null) return [];
641
- if (next === null) return ["unwind policy was removed"];
889
+ function diffQuotes(previous: unknown, next: unknown): string[] {
890
+ if (previous === null || previous === undefined) return [];
891
+ if (next === null || next === undefined) return ["quote policy was removed"];
642
892
  return stableStringify(previous) === stableStringify(next)
643
893
  ? []
644
894
  : [
645
- "unwind policy changed; the penalty schedule and refund destination are frozen once live",
895
+ "quote policy changed; the charge schedule, the frozen fields, and the refund destination are frozen once live",
646
896
  ];
647
897
  }
648
898
 
@@ -651,7 +901,7 @@ function aggregateInvariantKey(invariant: UdlAggregate): string {
651
901
  "childField" in invariant
652
902
  ? invariant.childField
653
903
  : `count${invariant.window ? `[${invariant.window.field} per ${invariant.window.days}d]` : ""}`;
654
- return `${invariant.childNounId}.${measure} within ${invariant.parentField} via ${invariant.childRefField} while ${[...invariant.childStatuses].sort().join(",")}`;
904
+ return `${invariant.childInstrumentId}.${measure} within ${invariant.parentField} via ${invariant.childRefField} while ${[...invariant.childStatuses].sort().join(",")}`;
655
905
  }
656
906
 
657
907
  function removedSetEntries(
@@ -689,11 +939,11 @@ function recordValue(value: unknown): Readonly<Record<string, unknown>> {
689
939
  function stableStringify(value: unknown, depth = 1): string {
690
940
  if (depth > UDL_LIMITS.maxDepth) {
691
941
  throw new UdlError([
692
- {
693
- code: "resource_limit",
694
- message: `UDL nesting exceeds ${UDL_LIMITS.maxDepth} levels`,
695
- path: "$",
696
- },
942
+ issue(
943
+ "UDL1004",
944
+ "$",
945
+ `UDL nesting exceeds ${UDL_LIMITS.maxDepth} levels`,
946
+ ),
697
947
  ]);
698
948
  }
699
949
  if (value === null || typeof value !== "object") {