@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.
- package/CHANGELOG.md +78 -25
- package/README.md +37 -99
- package/conformance/README.md +44 -41
- package/conformance/evolution/action-contract.expected.json +10 -0
- package/conformance/evolution/action-contract.live.udl +44 -0
- package/conformance/evolution/action-contract.next.udl +45 -0
- package/conformance/evolution/product-identity.expected.json +10 -0
- package/conformance/evolution/product-identity.live.udl +55 -0
- package/conformance/evolution/product-identity.next.udl +55 -0
- package/conformance/evolution/version-required.expected.json +10 -0
- package/conformance/evolution/version-required.live.udl +55 -0
- package/conformance/evolution/version-required.next.udl +58 -0
- package/conformance/invalid/action-without-transition.expected.json +10 -0
- package/conformance/invalid/{verb-without-transition.udl → action-without-transition.udl} +7 -2
- package/conformance/invalid/agent-description-too-long.expected.json +10 -0
- package/conformance/invalid/agent-description-too-long.udl +57 -0
- package/conformance/invalid/blank-title.expected.json +1 -1
- package/conformance/invalid/blank-title.udl +6 -2
- package/conformance/invalid/composition-dial-duplicate-key.expected.json +10 -0
- package/conformance/invalid/composition-dial-duplicate-key.udl +73 -0
- package/conformance/invalid/depth-budget.expected.json +1 -1
- package/conformance/invalid/duplicate-subject.expected.json +10 -0
- package/conformance/invalid/duplicate-subject.udl +230 -0
- package/conformance/invalid/forged-effects.expected.json +10 -0
- package/conformance/invalid/forged-effects.udl +76 -0
- package/conformance/invalid/format-version.expected.json +1 -1
- package/conformance/invalid/format-version.udl +6 -2
- package/conformance/invalid/instrument-id-not-snake-case.expected.json +10 -0
- package/conformance/invalid/{noun-id-not-snake-case.udl → instrument-id-not-snake-case.udl} +6 -2
- package/conformance/invalid/invalid-aggregate-gate-shape.expected.json +10 -0
- package/conformance/invalid/invalid-aggregate-gate-shape.udl +1819 -0
- package/conformance/invalid/invalid-check-duration.expected.json +10 -0
- package/conformance/invalid/invalid-check-duration.udl +219 -0
- package/conformance/invalid/invalid-dial-anchor.expected.json +10 -0
- package/conformance/invalid/invalid-dial-anchor.udl +219 -0
- package/conformance/invalid/invalid-exception-parent-ref.expected.json +10 -0
- package/conformance/invalid/invalid-exception-parent-ref.udl +2487 -0
- package/conformance/invalid/invalid-exposure-shape.expected.json +10 -0
- package/conformance/invalid/invalid-exposure-shape.udl +1819 -0
- package/conformance/invalid/invalid-remainder.expected.json +10 -0
- package/conformance/invalid/invalid-remainder.udl +220 -0
- package/conformance/invalid/invalid-schema-keyword.expected.json +10 -0
- package/conformance/invalid/invalid-schema-keyword.udl +220 -0
- package/conformance/invalid/invalid-utf8.expected.json +1 -1
- package/conformance/invalid/malformed-json.expected.json +1 -1
- package/conformance/invalid/missing-create-action.expected.json +10 -0
- package/conformance/invalid/{missing-create-verb.udl → missing-create-action.udl} +5 -2
- package/conformance/invalid/missing-exception-amount-field.expected.json +10 -0
- package/conformance/invalid/missing-exception-amount-field.udl +2487 -0
- package/conformance/invalid/missing-exception-contract.expected.json +14 -0
- package/conformance/invalid/missing-exception-contract.udl +2523 -0
- package/conformance/invalid/missing-exception-reason-field.expected.json +10 -0
- package/conformance/invalid/missing-exception-reason-field.udl +2487 -0
- package/conformance/invalid/not-an-object.expected.json +1 -1
- package/conformance/invalid/payout-reconcile-not-a-bank-debit.expected.json +10 -0
- package/conformance/invalid/payout-reconcile-not-a-bank-debit.udl +259 -0
- package/conformance/invalid/quote-freeze-set-incomplete.expected.json +10 -0
- package/conformance/invalid/quote-freeze-set-incomplete.udl +261 -0
- package/conformance/invalid/quote-named-reference-gate.expected.json +10 -0
- package/conformance/invalid/quote-named-reference-gate.udl +50 -0
- package/conformance/invalid/reconcile-named-reference-gate.expected.json +10 -0
- package/conformance/invalid/reconcile-named-reference-gate.udl +50 -0
- package/conformance/invalid/unknown-key.expected.json +1 -1
- package/conformance/invalid/unknown-key.udl +6 -2
- package/conformance/invalid/unknown-reference-gate-field.expected.json +10 -0
- package/conformance/invalid/unknown-reference-gate-field.udl +1819 -0
- package/conformance/invalid/unknown-required-field.expected.json +10 -0
- package/conformance/invalid/unknown-required-field.udl +220 -0
- package/conformance/invalid/unreachable-state.expected.json +2 -2
- package/conformance/invalid/unreachable-state.udl +6 -2
- package/conformance/invalid/wrong-exception-amount-field.expected.json +10 -0
- package/conformance/invalid/wrong-exception-amount-field.udl +2487 -0
- package/conformance/invalid/wrong-exception-reason-field.expected.json +10 -0
- package/conformance/invalid/wrong-exception-reason-field.udl +2487 -0
- package/conformance/valid/agent-description.expected.json +6 -0
- package/conformance/valid/agent-description.udl +66 -0
- package/conformance/valid/cards.expected.json +1 -0
- package/conformance/valid/cards.udl +843 -356
- package/conformance/valid/commerce-escrow.expected.json +1 -0
- package/conformance/valid/commerce-escrow.udl +792 -291
- package/conformance/valid/compiled-crowdfunding.expected.json +6 -0
- package/conformance/valid/compiled-crowdfunding.udl +1843 -0
- package/conformance/valid/compiled-watch-club.expected.json +6 -0
- package/conformance/valid/compiled-watch-club.udl +2486 -0
- package/conformance/valid/complete-contract.expected.json +6 -0
- package/conformance/valid/complete-contract.udl +218 -0
- package/conformance/valid/effect-signatures.expected.json +6 -0
- package/conformance/valid/effect-signatures.udl +75 -0
- package/conformance/valid/hand-edited.expected.json +1 -0
- package/conformance/valid/hand-edited.udl +1 -1
- package/conformance/valid/insured-car-marketplace.expected.json +1 -0
- package/conformance/valid/insured-car-marketplace.udl +534 -315
- package/conformance/valid/insured-travel.expected.json +2 -1
- package/conformance/valid/insured-travel.udl +1489 -641
- package/conformance/valid/minimal.expected.json +1 -0
- package/conformance/valid/minimal.udl +18 -14
- package/conformance/valid/protection.expected.json +1 -0
- package/conformance/valid/protection.udl +719 -304
- package/conformance/valid/string-escaping.expected.json +6 -0
- package/conformance/valid/string-escaping.udl +54 -0
- package/dist/canonical.d.ts +2 -0
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +7 -0
- package/dist/canonical.js.map +1 -1
- package/dist/check-profiles.d.ts +57 -0
- package/dist/check-profiles.d.ts.map +1 -0
- package/dist/check-profiles.js +62 -0
- package/dist/check-profiles.js.map +1 -0
- package/dist/cli.js +48 -4
- package/dist/cli.js.map +1 -1
- package/dist/diagnostics.d.ts +168 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +164 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/effects.d.ts +41 -0
- package/dist/effects.d.ts.map +1 -0
- package/dist/effects.js +110 -0
- package/dist/effects.js.map +1 -0
- package/dist/evolution.d.ts +51 -18
- package/dist/evolution.d.ts.map +1 -1
- package/dist/evolution.js +244 -102
- package/dist/evolution.js.map +1 -1
- package/dist/finance.d.ts +17 -13
- package/dist/finance.d.ts.map +1 -1
- package/dist/finance.js +165 -109
- package/dist/finance.js.map +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -5
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +2 -2
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +4 -3
- package/dist/limits.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +6 -16
- package/dist/parser.js.map +1 -1
- package/dist/schema.d.ts +2059 -135
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +759 -73
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts +22 -9
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +1463 -457
- package/dist/validation.js.map +1 -1
- package/docs/README.md +14 -0
- package/docs/guide/01-a-document.md +37 -0
- package/docs/guide/02-money-steps.md +23 -0
- package/docs/guide/03-laws.md +18 -0
- package/docs/guide/04-fees-and-remainder.md +15 -0
- package/docs/guide/05-checks-updates-dials.md +7 -0
- package/docs/guide/06-effects.md +11 -0
- package/docs/guide/07-evolution.md +11 -0
- package/docs/guide/08-implementing.md +13 -0
- package/docs/llms-full.txt +1241 -0
- package/docs/llms.txt +14 -0
- package/docs/reference/canonical.md +16 -0
- package/docs/reference/clauses.md +986 -0
- package/docs/reference/cli.md +22 -0
- package/docs/reference/diagnostics.md +32 -0
- package/package.json +6 -2
- package/skills/udl/SKILL.md +28 -0
- package/spec/README.md +82 -51
- package/spec/udl.schema.json +1191 -119
- package/src/canonical.ts +11 -0
- package/src/check-profiles.ts +80 -0
- package/src/cli.ts +49 -3
- package/src/diagnostics.ts +221 -0
- package/src/effects.ts +163 -0
- package/src/evolution.ts +375 -125
- package/src/finance.ts +255 -160
- package/src/index.ts +50 -14
- package/src/limits.ts +4 -3
- package/src/parser.ts +15 -18
- package/src/schema.ts +855 -81
- package/src/validation.ts +2666 -560
- package/conformance/invalid/missing-create-verb.expected.json +0 -10
- package/conformance/invalid/noun-id-not-snake-case.expected.json +0 -10
- 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
|
|
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
|
|
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,
|
|
34
|
-
*
|
|
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
|
|
41
|
-
readonly
|
|
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
|
|
48
|
-
readonly
|
|
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
|
|
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
|
|
65
|
-
readonly
|
|
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
|
-
|
|
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
|
|
120
|
+
readonly actions: Readonly<Record<string, EvolutionActionSnapshot>>;
|
|
81
121
|
}
|
|
82
122
|
|
|
83
|
-
/** Project an open .udl
|
|
84
|
-
export function
|
|
85
|
-
|
|
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
|
-
|
|
129
|
+
actionOrder: [...instrument.actionOrder],
|
|
130
|
+
aggregateInvariants: (instrument.aggregateInvariants ?? []).map(
|
|
88
131
|
aggregateInvariantKey,
|
|
89
132
|
),
|
|
90
|
-
|
|
91
|
-
derivedAmounts: (
|
|
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
|
-
|
|
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(
|
|
158
|
+
Object.entries(instrument.fields).map(([field, schema]) => [
|
|
98
159
|
field,
|
|
99
160
|
{ required: required.has(field), schema },
|
|
100
161
|
]),
|
|
101
162
|
),
|
|
102
|
-
id:
|
|
103
|
-
idPrefix:
|
|
104
|
-
initial:
|
|
105
|
-
|
|
106
|
-
|
|
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: [...
|
|
111
|
-
subjects:
|
|
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(
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
updateStates:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
176
|
-
previous.
|
|
264
|
+
const previousInstruments = new Map(
|
|
265
|
+
previous.instruments.map(
|
|
266
|
+
(instrument) =>
|
|
267
|
+
[instrument.id, snapshotUdlInstrument(instrument)] as const,
|
|
268
|
+
),
|
|
177
269
|
);
|
|
178
|
-
const
|
|
179
|
-
next.
|
|
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,
|
|
182
|
-
const current =
|
|
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
|
|
280
|
+
violations.push(`${id}: live instrument was removed from the product`);
|
|
186
281
|
continue;
|
|
187
282
|
}
|
|
188
|
-
if (stableStringify(
|
|
283
|
+
if (stableStringify(instrument) !== stableStringify(current)) {
|
|
189
284
|
semanticChange = true;
|
|
190
285
|
}
|
|
191
|
-
violations.push(...
|
|
286
|
+
violations.push(...diffInstrumentEvolutionMessages(instrument, current));
|
|
192
287
|
}
|
|
193
|
-
for (const id of
|
|
194
|
-
if (!
|
|
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
|
|
206
|
-
export function
|
|
207
|
-
previous:
|
|
208
|
-
next:
|
|
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 [`
|
|
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 [
|
|
233
|
-
const current = next.transitions[
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
262
|
-
stableStringify(next.
|
|
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
|
-
"
|
|
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(...
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
410
|
-
previous: Readonly<Record<string,
|
|
411
|
-
next: Readonly<Record<string,
|
|
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 [
|
|
415
|
-
const current = next[
|
|
631
|
+
for (const [action, descriptor] of Object.entries(previous)) {
|
|
632
|
+
const current = next[action];
|
|
416
633
|
if (!current) {
|
|
417
|
-
violations.push(`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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(
|
|
665
|
+
...diffInput(action, descriptor.input ?? {}, current.input ?? {}),
|
|
449
666
|
);
|
|
450
|
-
// A
|
|
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
|
-
`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
701
|
+
violations.push(`action ${action} changed its exposure admission gates`);
|
|
485
702
|
}
|
|
486
703
|
if (
|
|
487
|
-
descriptor.
|
|
488
|
-
stableStringify(descriptor.
|
|
489
|
-
stableStringify(current.
|
|
704
|
+
descriptor.reconcile !== undefined &&
|
|
705
|
+
stableStringify(descriptor.reconcile) !==
|
|
706
|
+
stableStringify(current.reconcile)
|
|
490
707
|
) {
|
|
491
|
-
violations.push(`
|
|
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(`
|
|
714
|
+
violations.push(`action ${action} changed its payout intent`);
|
|
498
715
|
}
|
|
499
716
|
if (descriptor.earnable !== current.earnable) {
|
|
500
|
-
violations.push(`
|
|
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(`
|
|
726
|
+
violations.push(`action ${action} changed its event name`);
|
|
504
727
|
}
|
|
505
728
|
if (stableStringify(descriptor.due) !== stableStringify(current.due)) {
|
|
506
|
-
violations.push(`
|
|
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(`
|
|
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(`
|
|
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
|
-
`
|
|
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(`
|
|
781
|
+
violations.push(`action ${action} changed its captured receipt input`);
|
|
532
782
|
}
|
|
533
783
|
if (stableStringify(descriptor.port) !== stableStringify(current.port)) {
|
|
534
|
-
violations.push(`
|
|
784
|
+
violations.push(`action ${action} changed its decision port`);
|
|
535
785
|
}
|
|
536
786
|
if (
|
|
537
|
-
descriptor.
|
|
538
|
-
descriptor.
|
|
787
|
+
descriptor.publicAction != null &&
|
|
788
|
+
descriptor.publicAction !== current.publicAction
|
|
539
789
|
) {
|
|
540
|
-
violations.push(`
|
|
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(`
|
|
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(`
|
|
802
|
+
violations.push(`action ${action} changed its signed child sum`);
|
|
553
803
|
}
|
|
554
804
|
}
|
|
555
|
-
for (const [
|
|
556
|
-
if (previous[
|
|
805
|
+
for (const [action, descriptor] of Object.entries(next)) {
|
|
806
|
+
if (previous[action] || descriptor.payout == null) continue;
|
|
557
807
|
violations.push(
|
|
558
|
-
`
|
|
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
|
-
|
|
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
|
|
574
|
-
// rejecting every caller that still sends it -- same law as
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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
|
-
`
|
|
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:
|
|
622
|
-
next:
|
|
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
|
|
640
|
-
if (previous === null) return [];
|
|
641
|
-
if (next === null) return ["
|
|
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
|
-
"
|
|
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.
|
|
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
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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") {
|