@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/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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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:
|
|
16
|
-
idPrefix:
|
|
17
|
-
initial:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
70
|
-
const
|
|
71
|
-
for (const [id,
|
|
72
|
-
const current =
|
|
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
|
|
113
|
+
violations.push(`${id}: live instrument was removed from the product`);
|
|
76
114
|
continue;
|
|
77
115
|
}
|
|
78
|
-
if (stableStringify(
|
|
116
|
+
if (stableStringify(instrument) !== stableStringify(current)) {
|
|
79
117
|
semanticChange = true;
|
|
80
118
|
}
|
|
81
|
-
violations.push(...
|
|
119
|
+
violations.push(...diffInstrumentEvolutionMessages(instrument, current));
|
|
82
120
|
}
|
|
83
|
-
for (const id of
|
|
84
|
-
if (!
|
|
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
|
|
93
|
-
export function
|
|
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 [`
|
|
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 [
|
|
106
|
-
const current = next.transitions[
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
124
|
-
stableStringify(next.
|
|
125
|
-
violations.push("
|
|
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(...
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
333
|
+
function diffActions(previous, next) {
|
|
211
334
|
const violations = [];
|
|
212
|
-
for (const [
|
|
213
|
-
const current = next[
|
|
335
|
+
for (const [action, descriptor] of Object.entries(previous)) {
|
|
336
|
+
const current = next[action];
|
|
214
337
|
if (!current) {
|
|
215
|
-
violations.push(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(
|
|
241
|
-
// A
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
386
|
+
violations.push(`action ${action} changed its exposure admission gates`);
|
|
264
387
|
}
|
|
265
|
-
if (descriptor.
|
|
266
|
-
stableStringify(descriptor.
|
|
267
|
-
stableStringify(current.
|
|
268
|
-
violations.push(`
|
|
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(`
|
|
395
|
+
violations.push(`action ${action} changed its payout intent`);
|
|
273
396
|
}
|
|
274
397
|
if (descriptor.earnable !== current.earnable) {
|
|
275
|
-
violations.push(`
|
|
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(`
|
|
405
|
+
violations.push(`action ${action} changed its event name`);
|
|
279
406
|
}
|
|
280
407
|
if (stableStringify(descriptor.due) !== stableStringify(current.due)) {
|
|
281
|
-
violations.push(`
|
|
408
|
+
violations.push(`action ${action} changed its due condition`);
|
|
282
409
|
}
|
|
283
410
|
if (stableStringify(descriptor.deadline) !== stableStringify(current.deadline)) {
|
|
284
|
-
violations.push(`
|
|
411
|
+
violations.push(`action ${action} changed its admission deadline`);
|
|
285
412
|
}
|
|
286
413
|
if (stableStringify(descriptor.decision) !== stableStringify(current.decision)) {
|
|
287
|
-
violations.push(`
|
|
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(`
|
|
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(`
|
|
442
|
+
violations.push(`action ${action} changed its captured receipt input`);
|
|
297
443
|
}
|
|
298
444
|
if (stableStringify(descriptor.port) !== stableStringify(current.port)) {
|
|
299
|
-
violations.push(`
|
|
445
|
+
violations.push(`action ${action} changed its decision port`);
|
|
300
446
|
}
|
|
301
|
-
if (descriptor.
|
|
302
|
-
descriptor.
|
|
303
|
-
violations.push(`
|
|
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(`
|
|
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(`
|
|
457
|
+
violations.push(`action ${action} changed its signed child sum`);
|
|
312
458
|
}
|
|
313
459
|
}
|
|
314
|
-
for (const [
|
|
315
|
-
if (previous[
|
|
460
|
+
for (const [action, descriptor] of Object.entries(next)) {
|
|
461
|
+
if (previous[action] || descriptor.payout == null)
|
|
316
462
|
continue;
|
|
317
|
-
violations.push(`
|
|
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(
|
|
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
|
|
327
|
-
// rejecting every caller that still sends it -- same law as
|
|
328
|
-
violations.push(`
|
|
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(`
|
|
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(`
|
|
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(`
|
|
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
|
|
366
|
-
if (previous === null)
|
|
511
|
+
function diffQuotes(previous, next) {
|
|
512
|
+
if (previous === null || previous === undefined)
|
|
367
513
|
return [];
|
|
368
|
-
if (next === null)
|
|
369
|
-
return ["
|
|
514
|
+
if (next === null || next === undefined)
|
|
515
|
+
return ["quote policy was removed"];
|
|
370
516
|
return stableStringify(previous) === stableStringify(next)
|
|
371
517
|
? []
|
|
372
518
|
: [
|
|
373
|
-
"
|
|
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.
|
|
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") {
|