@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/dist/evolution.js CHANGED
@@ -1,34 +1,62 @@
1
1
  import { UDL_LIMITS } from "./limits.js";
2
+ import { deriveUdlActionEffects } from "./effects.js";
3
+ import { udlClauseVocabulary } from "./schema.js";
2
4
  import { UdlError } from "./validation.js";
3
- /** Project an open .udl noun into the evolution engine's exact semantic shape. */
4
- export function snapshotUdlNoun(noun) {
5
- const required = new Set(noun.required);
5
+ import { issue } from "./diagnostics.js";
6
+ /** Project an open .udl instrument into the evolution engine's exact semantic shape. */
7
+ export function snapshotUdlInstrument(instrument) {
8
+ const required = new Set(instrument.required);
6
9
  return {
7
- aggregateInvariants: (noun.aggregateInvariants ?? []).map(aggregateInvariantKey),
8
- computedMoneyRefs: [...(noun.computedMoneyRefs ?? [])].sort(),
9
- derivedAmounts: (noun.derivedAmounts ?? []).map((amount) => `${amount.field}=floor(${amount.sourceField}*${amount.rule.bps}/10000)`),
10
- ...(noun.distinctParties ? { distinctParties: true } : {}),
11
- fields: Object.fromEntries(Object.entries(noun.fields).map(([field, schema]) => [
10
+ actionOrder: [...instrument.actionOrder],
11
+ aggregateInvariants: (instrument.aggregateInvariants ?? []).map(aggregateInvariantKey),
12
+ callerParkedStates: instrument.callerParkedStates ?? {},
13
+ derivedAmounts: (instrument.derivedAmounts ?? []).map((amount) => `${amount.field}=floor(${amount.sourceField}*${amount.rule.bps}/10000)`),
14
+ dials: instrument.dials ?? [],
15
+ ...(instrument.distinctParties ? { distinctParties: true } : {}),
16
+ ...(instrument.feeRules
17
+ ? {
18
+ feeRules: instrument.feeRules.map((fee) => ({
19
+ ...fee,
20
+ rule: fee.rule.kind === "tiered"
21
+ ? {
22
+ ...fee.rule,
23
+ tiers: fee.rule.tiers.map((tier) => ({
24
+ ...tier,
25
+ rule: { ...tier.rule },
26
+ })),
27
+ }
28
+ : { ...fee.rule },
29
+ })),
30
+ }
31
+ : {}),
32
+ fields: Object.fromEntries(Object.entries(instrument.fields).map(([field, schema]) => [
12
33
  field,
13
34
  { required: required.has(field), schema },
14
35
  ])),
15
- id: noun.id,
16
- idPrefix: noun.idPrefix,
17
- initial: noun.lifecycle.initial,
18
- parties: compactStringRecord(noun.parties),
19
- partitions: (noun.partitions ?? []).map((partition) => `${partition.totalField} = ${[...partition.pieceFields].sort().join(" + ")}`),
20
- states: [...noun.lifecycle.states],
21
- subjects: noun.subject ? [...noun.subject.kinds] : [],
22
- transitions: Object.fromEntries(Object.entries(noun.lifecycle.transitions).map(([verb, transition]) => [
23
- verb,
36
+ id: instrument.id,
37
+ idPrefix: instrument.idPrefix,
38
+ initial: instrument.lifecycle.initial,
39
+ nav: instrument.nav ?? [],
40
+ parties: compactStringRecord(instrument.parties),
41
+ partitions: (instrument.partitions ?? []).map((partition) => `${partition.totalField} = ${[...partition.pieceFields].sort().join(" + ")}`),
42
+ states: [...instrument.lifecycle.states],
43
+ subjects: instrument.subject ? [...instrument.subject.kinds] : [],
44
+ subjectExtensible: instrument.subject?.extensible ?? false,
45
+ surfaceVisibility: instrument.surfaceVisibility ?? null,
46
+ templateId: instrument.templateId ?? null,
47
+ transitions: Object.fromEntries(Object.entries(instrument.lifecycle.transitions).map(([action, transition]) => [
48
+ action,
24
49
  { from: [...transition.from], to: transition.to },
25
50
  ])),
26
- unwind: noun.unwind ?? null,
27
- updateFields: noun.update?.fields ?? [],
28
- updateStates: noun.update?.states ?? [],
29
- verbs: Object.fromEntries(Object.entries(noun.verbs).map(([verb, definition]) => [
30
- verb,
31
- snapshotUdlVerb(definition),
51
+ quotes: Object.fromEntries(Object.entries(instrument.actions)
52
+ .filter(([, action]) => action.quote)
53
+ .map(([name, action]) => [name, action.quote])),
54
+ updateFields: instrument.update?.fields ?? [],
55
+ updateExamples: instrument.update?.examples ?? null,
56
+ updateStates: instrument.update?.states ?? [],
57
+ actions: Object.fromEntries(instrument.actionOrder.map((action) => [
58
+ action,
59
+ snapshotUdlAction(instrument.actions[action]),
32
60
  ])),
33
61
  };
34
62
  }
@@ -41,6 +69,16 @@ export function snapshotUdlNoun(noun) {
41
69
  * clean `resource_limit` issue: parse first, then diff.
42
70
  */
43
71
  export function diffValidatedUdlEvolution(previous, next) {
72
+ return diffValidatedUdlEvolutionMessages(previous, next).map((message) => {
73
+ const instrumentId = /^([^:]+): /.exec(message)?.[1];
74
+ const instrumentIndex = previous.instruments.findIndex((instrument) => instrument.id === instrumentId);
75
+ const base = instrumentIndex < 0
76
+ ? "$.instruments"
77
+ : `$.instruments[${instrumentIndex}]`;
78
+ return evolutionIssue(message, base);
79
+ });
80
+ }
81
+ function diffValidatedUdlEvolutionMessages(previous, next) {
44
82
  const violations = [];
45
83
  if (previous.product !== next.product) {
46
84
  violations.push(`product id changed from ${previous.product} to ${next.product}`);
@@ -66,22 +104,22 @@ export function diffValidatedUdlEvolution(previous, next) {
66
104
  if (!previousSubjects.has(kind))
67
105
  semanticChange = true;
68
106
  }
69
- const previousNouns = new Map(previous.nouns.map((noun) => [noun.id, snapshotUdlNoun(noun)]));
70
- const nextNouns = new Map(next.nouns.map((noun) => [noun.id, snapshotUdlNoun(noun)]));
71
- for (const [id, noun] of previousNouns) {
72
- const current = nextNouns.get(id);
107
+ const previousInstruments = new Map(previous.instruments.map((instrument) => [instrument.id, snapshotUdlInstrument(instrument)]));
108
+ const nextInstruments = new Map(next.instruments.map((instrument) => [instrument.id, snapshotUdlInstrument(instrument)]));
109
+ for (const [id, instrument] of previousInstruments) {
110
+ const current = nextInstruments.get(id);
73
111
  if (!current) {
74
112
  semanticChange = true;
75
- violations.push(`${id}: live noun was removed from the product`);
113
+ violations.push(`${id}: live instrument was removed from the product`);
76
114
  continue;
77
115
  }
78
- if (stableStringify(noun) !== stableStringify(current)) {
116
+ if (stableStringify(instrument) !== stableStringify(current)) {
79
117
  semanticChange = true;
80
118
  }
81
- violations.push(...diffNounEvolution(noun, current));
119
+ violations.push(...diffInstrumentEvolutionMessages(instrument, current));
82
120
  }
83
- for (const id of nextNouns.keys()) {
84
- if (!previousNouns.has(id))
121
+ for (const id of nextInstruments.keys()) {
122
+ if (!previousInstruments.has(id))
85
123
  semanticChange = true;
86
124
  }
87
125
  if (semanticChange && next.version <= previous.version) {
@@ -89,10 +127,13 @@ export function diffValidatedUdlEvolution(previous, next) {
89
127
  }
90
128
  return violations;
91
129
  }
92
- /** Diff one noun snapshot. An empty result means the change is additive. */
93
- export function diffNounEvolution(previous, next) {
130
+ /** Diff one instrument snapshot. An empty result means the change is additive. */
131
+ export function diffInstrumentEvolution(previous, next) {
132
+ return diffInstrumentEvolutionMessages(previous, next).map((message) => evolutionIssue(message, "$.instruments"));
133
+ }
134
+ function diffInstrumentEvolutionMessages(previous, next) {
94
135
  if (previous.id !== next.id) {
95
- return [`noun id changed from ${previous.id} to ${next.id}`];
136
+ return [`instrument id changed from ${previous.id} to ${next.id}`];
96
137
  }
97
138
  const violations = [];
98
139
  if (previous.idPrefix !== next.idPrefix) {
@@ -102,47 +143,129 @@ export function diffNounEvolution(previous, next) {
102
143
  violations.push(`initial lifecycle state changed from ${previous.initial} to ${next.initial}`);
103
144
  }
104
145
  violations.push(...removedSetEntries(previous.states, next.states, (state) => `lifecycle state ${state} was removed or renamed`));
105
- for (const [verb, transition] of Object.entries(previous.transitions)) {
106
- const current = next.transitions[verb];
146
+ for (const [action, transition] of Object.entries(previous.transitions)) {
147
+ const current = next.transitions[action];
107
148
  if (!current) {
108
- violations.push(`transition for verb ${verb} was removed or renamed`);
149
+ violations.push(`transition for action ${action} was removed or renamed`);
109
150
  continue;
110
151
  }
111
152
  if (transition.to !== current.to) {
112
- violations.push(`transition for verb ${verb} changed its target state from ${transition.to} to ${current.to}`);
153
+ violations.push(`transition for action ${action} changed its target state from ${transition.to} to ${current.to}`);
113
154
  }
114
- violations.push(...removedSetEntries(transition.from, current.from, (state) => `transition for verb ${verb} no longer fires from state ${state}`));
155
+ violations.push(...removedSetEntries(transition.from, current.from, (state) => `transition for action ${action} no longer fires from state ${state}`));
115
156
  }
116
157
  violations.push(...diffFields(previous.fields, next.fields));
117
158
  if (Boolean(previous.distinctParties) !== Boolean(next.distinctParties)) {
118
159
  violations.push("distinct-party admission changed after becoming live");
119
160
  }
120
- violations.push(...diffVerbs(previous.verbs, next.verbs));
161
+ if (previous.actionOrder) {
162
+ const previousActions = new Set(previous.actionOrder);
163
+ const retainedActionOrder = (next.actionOrder ?? []).filter((action) => previousActions.has(action));
164
+ if (stableStringify(previous.actionOrder) !==
165
+ stableStringify(retainedActionOrder)) {
166
+ violations.push("instrument action order changed after becoming live");
167
+ }
168
+ }
169
+ violations.push(...diffActions(previous.actions, next.actions));
121
170
  violations.push(...diffParties(previous.parties, next.parties));
122
171
  violations.push(...diffAggregates(previous, next));
123
- if (stableStringify(previous.computedMoneyRefs ?? []) !==
124
- stableStringify(next.computedMoneyRefs ?? [])) {
125
- violations.push("computed money refs changed; runtime-derived money surfaces are frozen once live");
172
+ if (stableStringify(Object.keys(previous.callerParkedStates ?? {}).sort()) !==
173
+ stableStringify(Object.keys(next.callerParkedStates ?? {}).sort())) {
174
+ violations.push("caller-parked state annotations changed");
175
+ }
176
+ if (stableStringify(previous.dials ?? []) !== stableStringify(next.dials ?? [])) {
177
+ violations.push("instrument dials changed after becoming live");
178
+ }
179
+ if ((previous.surfaceVisibility ?? null) !== (next.surfaceVisibility ?? null)) {
180
+ violations.push("instrument surface visibility changed after becoming live");
181
+ }
182
+ if ((previous.templateId ?? null) !== (next.templateId ?? null)) {
183
+ violations.push("instrument archetype template changed after becoming live");
184
+ }
185
+ if (previous.subjectExtensible && !next.subjectExtensible) {
186
+ violations.push("tenant-defined subject kinds are no longer accepted");
126
187
  }
127
188
  if (stableStringify(previous.derivedAmounts ?? []) !==
128
189
  stableStringify(next.derivedAmounts ?? [])) {
129
190
  violations.push("derived amount rules changed; derived money arithmetic is frozen once live");
130
191
  }
192
+ if (stableStringify(previous.feeRules ?? []) !==
193
+ stableStringify(next.feeRules ?? [])) {
194
+ violations.push("feeRules changed; fee calculation and settlement funding are frozen once live");
195
+ }
131
196
  violations.push(...removedSetEntries(previous.partitions, next.partitions, (law) => `partition law [${law}] was removed`), ...next.partitions
132
197
  .filter((law) => !previous.partitions.includes(law))
133
198
  .map((law) => `partition law [${law}] was added, which can reject existing callers`));
134
- violations.push(...diffUnwind(previous.unwind, next.unwind));
199
+ violations.push(...diffQuotes(previous.quotes, next.quotes));
135
200
  violations.push(...removedSetEntries(previous.subjects, next.subjects, (kind) => `subject kind ${kind} was removed, rejecting linked instances`), ...removedSetEntries(previous.updateFields, next.updateFields, (field) => `update policy no longer permits field ${field}`), ...removedSetEntries(previous.updateStates, next.updateStates, (state) => `update policy no longer permits state ${state}`));
136
201
  return violations.map((violation) => `${next.id}: ${violation}`);
137
202
  }
138
- function snapshotUdlVerb(definition) {
203
+ function evolutionIssue(message, instrumentBase) {
204
+ if (message.startsWith("product id changed"))
205
+ return issue("UDL7001", "$.product", message);
206
+ if (message.startsWith("product version moved backward"))
207
+ return issue("UDL7001", "$.version", message);
208
+ if (message.startsWith("product definition changed"))
209
+ return issue("UDL7002", "$.version", message);
210
+ if (message.startsWith("subject kind "))
211
+ return issue("UDL7001", "$.subjects", message);
212
+ const instrumentMatch = /^([^:]+): (.*)$/.exec(message);
213
+ const detail = instrumentMatch?.[2] ?? message;
214
+ const transitionAction = /^transition for action ([a-z][a-z0-9_]*)/.exec(detail)?.[1];
215
+ if (transitionAction)
216
+ return issue("UDL7001", `${instrumentBase}.lifecycle.transitions.${transitionAction}`, message);
217
+ const action = /^action ([a-z][a-z0-9_]*)/.exec(detail)?.[1];
218
+ if (action)
219
+ return issue("UDL7001", `${instrumentBase}.actions.${action}`, message);
220
+ if (detail.includes("instrument id"))
221
+ return issue("UDL7001", instrumentBase, message);
222
+ if (detail.includes("id prefix") ||
223
+ detail.includes("lifecycle") ||
224
+ detail.includes("transition") ||
225
+ detail.includes("initial") ||
226
+ detail.includes("live instrument was removed"))
227
+ return issue("UDL7001", `${instrumentBase}.lifecycle`, message);
228
+ if (detail.startsWith("field "))
229
+ return issue("UDL7001", `${instrumentBase}.fields`, message);
230
+ if (detail.includes("move") ||
231
+ detail.includes("payout") ||
232
+ detail.includes("fee") ||
233
+ detail.includes("quote") ||
234
+ detail.includes("derived amount") ||
235
+ detail.includes("partition") ||
236
+ detail.includes("distribution") ||
237
+ detail.includes("signed child sum") ||
238
+ detail.includes("earnable"))
239
+ return issue("UDL7001", `${instrumentBase}.actions`, message);
240
+ if (detail.includes("gate") ||
241
+ detail.includes("check prerequisite") ||
242
+ detail.includes("reconcile expectation") ||
243
+ detail.includes("drained-account") ||
244
+ detail.includes("exposure") ||
245
+ detail.includes("aggregate"))
246
+ return issue("UDL7001", `${instrumentBase}.actions`, message);
247
+ if (detail.startsWith("action ") ||
248
+ detail.includes("action order") ||
249
+ detail.includes("step count"))
250
+ return issue("UDL7001", `${instrumentBase}.actions`, message);
251
+ return issue("UDL7001", instrumentBase, message);
252
+ }
253
+ function snapshotUdlAction(definition) {
139
254
  return {
140
255
  captureInput: definition.captureInput ?? null,
256
+ commit: definition.commit ?? null,
141
257
  deadline: definition.deadline ?? null,
142
258
  decision: definition.decision ?? null,
259
+ principal: definition.principal ?? null,
260
+ remainder: definition.remainder ?? null,
261
+ requiresChecks: definition.requiresChecks ?? [],
262
+ sandboxFailurePoint: definition.sandboxFailurePoint ?? null,
263
+ updates: definition.updates ?? [],
143
264
  distribute: definition.distribute ?? null,
144
265
  due: definition.due ?? null,
145
266
  earnable: definition.earnable ?? false,
267
+ effects: definition.effects ??
268
+ deriveUdlActionEffects(definition, udlClauseVocabulary),
146
269
  eventName: definition.eventName ?? null,
147
270
  input: definition.input ? snapshotJsonSchemaFields(definition.input) : null,
148
271
  inputConstraints: definition.input
@@ -150,7 +273,7 @@ function snapshotUdlVerb(definition) {
150
273
  : null,
151
274
  payout: definition.payout ?? null,
152
275
  port: definition.port ?? null,
153
- publicIntent: definition.publicIntent ?? null,
276
+ publicAction: definition.publicAction ?? null,
154
277
  setsAt: definition.setsAt ?? null,
155
278
  signedSum: definition.signedSum ?? null,
156
279
  moves: definition.moves.map((move) => ({
@@ -163,7 +286,7 @@ function snapshotUdlVerb(definition) {
163
286
  requiresDrainedAccount: definition.requiresDrainedAccount ?? null,
164
287
  requiresExposure: definition.requiresExposure ?? [],
165
288
  requiresRefs: definition.requiresRefs ?? [],
166
- requiresSettlement: definition.requiresSettlement ?? null,
289
+ reconcile: definition.reconcile ?? null,
167
290
  steps: definition.steps.map((step) => ({
168
291
  bind: step.bind,
169
292
  capture: step.capture ?? {},
@@ -207,137 +330,160 @@ function diffFields(previous, next) {
207
330
  }
208
331
  return violations;
209
332
  }
210
- function diffVerbs(previous, next) {
333
+ function diffActions(previous, next) {
211
334
  const violations = [];
212
- for (const [verb, descriptor] of Object.entries(previous)) {
213
- const current = next[verb];
335
+ for (const [action, descriptor] of Object.entries(previous)) {
336
+ const current = next[action];
214
337
  if (!current) {
215
- violations.push(`verb ${verb} was removed or renamed`);
338
+ violations.push(`action ${action} was removed or renamed`);
216
339
  continue;
217
340
  }
218
341
  if (descriptor.moves.length !== current.moves.length) {
219
- violations.push(`verb ${verb} changed its move count from ${descriptor.moves.length} to ${current.moves.length}; money movement is frozen once live`);
342
+ violations.push(`action ${action} changed its move count from ${descriptor.moves.length} to ${current.moves.length}; money movement is frozen once live`);
220
343
  }
221
344
  else {
222
345
  descriptor.moves.forEach((move, index) => {
223
346
  const nextMove = current.moves[index];
224
347
  if (stableStringify(move) !== stableStringify(nextMove)) {
225
- violations.push(`verb ${verb} changed move ${move.key} at index ${index}; money movement is frozen once live`);
348
+ violations.push(`action ${action} changed move ${move.key} at index ${index}; money movement is frozen once live`);
226
349
  }
227
350
  });
228
351
  }
229
352
  if (descriptor.steps.length !== current.steps.length) {
230
- violations.push(`verb ${verb} changed its step count from ${descriptor.steps.length} to ${current.steps.length}; a live verb's steps are frozen`);
353
+ violations.push(`action ${action} changed its step count from ${descriptor.steps.length} to ${current.steps.length}; a live action's steps are frozen`);
231
354
  }
232
355
  else {
233
356
  descriptor.steps.forEach((step, index) => {
234
357
  const nextStep = current.steps[index];
235
358
  if (stableStringify(step) === stableStringify(nextStep))
236
359
  return;
237
- violations.push(`verb ${verb} changed step ${index} (${step.operation}); a live verb's steps are frozen`);
360
+ violations.push(`action ${action} changed step ${index} (${step.operation}); a live action's steps are frozen`);
238
361
  });
239
362
  }
240
- violations.push(...diffInput(verb, descriptor.input ?? {}, current.input ?? {}));
241
- // A verb gaining its first input is additive (per-field checks still catch
363
+ violations.push(...diffInput(action, descriptor.input ?? {}, current.input ?? {}));
364
+ // A action gaining its first input is additive (per-field checks still catch
242
365
  // added-as-required); once an input is live, its schema envelope is frozen.
243
366
  if (descriptor.input !== null &&
244
367
  stableStringify(descriptor.inputConstraints) !==
245
368
  stableStringify(current.inputConstraints)) {
246
- violations.push(`verb ${verb} changed its input schema beyond declared fields; a live verb input is frozen`);
369
+ violations.push(`action ${action} changed its input schema beyond declared fields; a live action input is frozen`);
247
370
  }
248
371
  if (stableStringify(descriptor.requiresRefs) !==
249
372
  stableStringify(current.requiresRefs)) {
250
- violations.push(`verb ${verb} changed its cross-noun gates`);
373
+ violations.push(`action ${action} changed its cross-instrument gates`);
251
374
  }
252
375
  if (stableStringify(descriptor.requiresAggregate) !==
253
376
  stableStringify(current.requiresAggregate)) {
254
- violations.push(`verb ${verb} changed its aggregate admission gates`);
377
+ violations.push(`action ${action} changed its aggregate admission gates`);
255
378
  }
256
379
  if (stableStringify(descriptor.requiresDrainedAccount) !==
257
380
  stableStringify(current.requiresDrainedAccount)) {
258
- violations.push(`verb ${verb} changed its drained-account gate`);
381
+ violations.push(`action ${action} changed its drained-account gate`);
259
382
  }
260
383
  if (descriptor.requiresExposure !== undefined &&
261
384
  stableStringify(descriptor.requiresExposure) !==
262
385
  stableStringify(current.requiresExposure)) {
263
- violations.push(`verb ${verb} changed its exposure admission gates`);
386
+ violations.push(`action ${action} changed its exposure admission gates`);
264
387
  }
265
- if (descriptor.requiresSettlement !== undefined &&
266
- stableStringify(descriptor.requiresSettlement) !==
267
- stableStringify(current.requiresSettlement)) {
268
- violations.push(`verb ${verb} changed its settlement evidence gate`);
388
+ if (descriptor.reconcile !== undefined &&
389
+ stableStringify(descriptor.reconcile) !==
390
+ stableStringify(current.reconcile)) {
391
+ violations.push(`action ${action} changed its reconcile expectations`);
269
392
  }
270
393
  if (stableStringify(descriptor.payout ?? null) !==
271
394
  stableStringify(current.payout ?? null)) {
272
- violations.push(`verb ${verb} changed its payout intent`);
395
+ violations.push(`action ${action} changed its payout intent`);
273
396
  }
274
397
  if (descriptor.earnable !== current.earnable) {
275
- violations.push(`verb ${verb} changed its earnable flag`);
398
+ violations.push(`action ${action} changed its earnable flag`);
399
+ }
400
+ if (descriptor.effects !== undefined &&
401
+ stableStringify(descriptor.effects) !== stableStringify(current.effects)) {
402
+ violations.push(`action ${action} changed its derived effects`);
276
403
  }
277
404
  if (descriptor.eventName !== current.eventName) {
278
- violations.push(`verb ${verb} changed its event name`);
405
+ violations.push(`action ${action} changed its event name`);
279
406
  }
280
407
  if (stableStringify(descriptor.due) !== stableStringify(current.due)) {
281
- violations.push(`verb ${verb} changed its due condition`);
408
+ violations.push(`action ${action} changed its due condition`);
282
409
  }
283
410
  if (stableStringify(descriptor.deadline) !== stableStringify(current.deadline)) {
284
- violations.push(`verb ${verb} changed its admission deadline`);
411
+ violations.push(`action ${action} changed its admission deadline`);
285
412
  }
286
413
  if (stableStringify(descriptor.decision) !== stableStringify(current.decision)) {
287
- violations.push(`verb ${verb} changed its provider decision`);
414
+ violations.push(`action ${action} changed its provider decision`);
415
+ }
416
+ if ((descriptor.principal ?? null) !== (current.principal ?? null)) {
417
+ violations.push(`action ${action} changed its caller principal`);
418
+ }
419
+ if (stableStringify(descriptor.remainder ?? null) !==
420
+ stableStringify(current.remainder ?? null)) {
421
+ violations.push(`action ${action} changed its remainder rule`);
422
+ }
423
+ if (stableStringify(descriptor.requiresChecks ?? []) !==
424
+ stableStringify(current.requiresChecks ?? [])) {
425
+ violations.push(`action ${action} changed its check prerequisites`);
426
+ }
427
+ if ((descriptor.sandboxFailurePoint ?? null) !==
428
+ (current.sandboxFailurePoint ?? null)) {
429
+ violations.push(`action ${action} changed its sandbox failure point`);
430
+ }
431
+ if (stableStringify(descriptor.updates ?? []) !==
432
+ stableStringify(current.updates ?? [])) {
433
+ violations.push(`action ${action} changed its field updates`);
288
434
  }
289
435
  if (stableStringify(descriptor.distribute) !==
290
436
  stableStringify(current.distribute)) {
291
- violations.push(`verb ${verb} changed its distribution rule; money distribution is frozen once live`);
437
+ violations.push(`action ${action} changed its distribution rule; money distribution is frozen once live`);
292
438
  }
293
439
  if (descriptor.captureInput !== undefined &&
294
440
  stableStringify(descriptor.captureInput) !==
295
441
  stableStringify(current.captureInput)) {
296
- violations.push(`verb ${verb} changed its captured receipt input`);
442
+ violations.push(`action ${action} changed its captured receipt input`);
297
443
  }
298
444
  if (stableStringify(descriptor.port) !== stableStringify(current.port)) {
299
- violations.push(`verb ${verb} changed its decision port`);
445
+ violations.push(`action ${action} changed its decision port`);
300
446
  }
301
- if (descriptor.publicIntent != null &&
302
- descriptor.publicIntent !== current.publicIntent) {
303
- violations.push(`verb ${verb} changed its public intent`);
447
+ if (descriptor.publicAction != null &&
448
+ descriptor.publicAction !== current.publicAction) {
449
+ violations.push(`action ${action} changed its public action`);
304
450
  }
305
451
  if (stableStringify(descriptor.setsAt) !== stableStringify(current.setsAt)) {
306
- violations.push(`verb ${verb} changed its computed timestamp`);
452
+ violations.push(`action ${action} changed its computed timestamp`);
307
453
  }
308
454
  if (descriptor.signedSum !== undefined &&
309
455
  stableStringify(descriptor.signedSum) !==
310
456
  stableStringify(current.signedSum)) {
311
- violations.push(`verb ${verb} changed its signed child sum`);
457
+ violations.push(`action ${action} changed its signed child sum`);
312
458
  }
313
459
  }
314
- for (const [verb, descriptor] of Object.entries(next)) {
315
- if (previous[verb] || descriptor.payout == null)
460
+ for (const [action, descriptor] of Object.entries(next)) {
461
+ if (previous[action] || descriptor.payout == null)
316
462
  continue;
317
- violations.push(`verb ${verb} added a payout intent; external money movement is frozen once live`);
463
+ violations.push(`action ${action} added a payout intent; external money movement is frozen once live`);
318
464
  }
319
465
  return violations;
320
466
  }
321
- function diffInput(verb, previous, next) {
467
+ function diffInput(action, previous, next) {
322
468
  const violations = [];
323
469
  for (const [field, descriptor] of Object.entries(previous)) {
324
470
  const current = next[field];
325
471
  if (!current) {
326
- // Compiled verb inputs are strict objects, so a removed field starts
327
- // rejecting every caller that still sends it -- same law as noun fields.
328
- violations.push(`verb ${verb} input field ${field} was removed or renamed`);
472
+ // Compiled action inputs are strict objects, so a removed field starts
473
+ // rejecting every caller that still sends it -- same law as instrument fields.
474
+ violations.push(`action ${action} input field ${field} was removed or renamed`);
329
475
  continue;
330
476
  }
331
477
  if (!descriptor.required && current.required) {
332
- violations.push(`verb ${verb} input field ${field} became required, tightening the verb input`);
478
+ violations.push(`action ${action} input field ${field} became required, tightening the action input`);
333
479
  }
334
480
  if (stableStringify(descriptor.schema) !== stableStringify(current.schema)) {
335
- violations.push(`verb ${verb} input field ${field} schema changed; a live verb input schema is frozen`);
481
+ violations.push(`action ${action} input field ${field} schema changed; a live action input schema is frozen`);
336
482
  }
337
483
  }
338
484
  for (const [field, descriptor] of Object.entries(next)) {
339
485
  if (!previous[field] && descriptor.required) {
340
- violations.push(`verb ${verb} input field ${field} was added as required, tightening the verb input`);
486
+ violations.push(`action ${action} input field ${field} was added as required, tightening the action input`);
341
487
  }
342
488
  }
343
489
  return violations;
@@ -362,22 +508,22 @@ function diffAggregates(previous, next) {
362
508
  .map((key) => `aggregate invariant [${key}] was added, which can reject existing instances`),
363
509
  ];
364
510
  }
365
- function diffUnwind(previous, next) {
366
- if (previous === null)
511
+ function diffQuotes(previous, next) {
512
+ if (previous === null || previous === undefined)
367
513
  return [];
368
- if (next === null)
369
- return ["unwind policy was removed"];
514
+ if (next === null || next === undefined)
515
+ return ["quote policy was removed"];
370
516
  return stableStringify(previous) === stableStringify(next)
371
517
  ? []
372
518
  : [
373
- "unwind policy changed; the penalty schedule and refund destination are frozen once live",
519
+ "quote policy changed; the charge schedule, the frozen fields, and the refund destination are frozen once live",
374
520
  ];
375
521
  }
376
522
  function aggregateInvariantKey(invariant) {
377
523
  const measure = "childField" in invariant
378
524
  ? invariant.childField
379
525
  : `count${invariant.window ? `[${invariant.window.field} per ${invariant.window.days}d]` : ""}`;
380
- return `${invariant.childNounId}.${measure} within ${invariant.parentField} via ${invariant.childRefField} while ${[...invariant.childStatuses].sort().join(",")}`;
526
+ return `${invariant.childInstrumentId}.${measure} within ${invariant.parentField} via ${invariant.childRefField} while ${[...invariant.childStatuses].sort().join(",")}`;
381
527
  }
382
528
  function removedSetEntries(previous, next, message) {
383
529
  const current = new Set(next);
@@ -401,11 +547,7 @@ function recordValue(value) {
401
547
  function stableStringify(value, depth = 1) {
402
548
  if (depth > UDL_LIMITS.maxDepth) {
403
549
  throw new UdlError([
404
- {
405
- code: "resource_limit",
406
- message: `UDL nesting exceeds ${UDL_LIMITS.maxDepth} levels`,
407
- path: "$",
408
- },
550
+ issue("UDL1004", "$", `UDL nesting exceeds ${UDL_LIMITS.maxDepth} levels`),
409
551
  ]);
410
552
  }
411
553
  if (value === null || typeof value !== "object") {