@hyperscale0/udl 1.0.125 → 1.0.130
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/dist/field-value.d.ts.map +1 -1
- package/dist/field-value.js +7 -2
- package/dist/field-value.js.map +1 -1
- package/dist/finance.d.ts.map +1 -1
- package/dist/finance.js +87 -6
- package/dist/finance.js.map +1 -1
- package/dist/object-semantics-schema.d.ts +359 -0
- package/dist/object-semantics-schema.d.ts.map +1 -1
- package/dist/object-semantics.d.ts +3 -2
- package/dist/object-semantics.d.ts.map +1 -1
- package/dist/object-semantics.js +3 -2
- package/dist/object-semantics.js.map +1 -1
- package/dist/schema.d.ts +386 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +13 -0
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +14 -0
- package/dist/validation.js.map +1 -1
- package/package.json +2 -2
- package/spec/README.md +11 -3
- package/spec/udl.schema.json +65 -0
- package/src/field-value.ts +8 -2
- package/src/finance.ts +113 -9
- package/src/object-semantics.ts +3 -2
- package/src/schema.ts +13 -0
- package/src/validation.ts +22 -0
package/src/finance.ts
CHANGED
|
@@ -98,9 +98,42 @@ export function analyzeInstrumentFinance(
|
|
|
98
98
|
const problems: FinanceIssue[] = [];
|
|
99
99
|
const report = (message: string) =>
|
|
100
100
|
problems.push({ path: ".actions", message });
|
|
101
|
+
// A field some action sets is a free symbol: its default no longer holds.
|
|
102
|
+
const assigned = new Set(
|
|
103
|
+
Object.values(instrument.actions).flatMap((action) =>
|
|
104
|
+
Object.keys(action.set ?? {}).map((key) => `self.${key}`),
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
const reads = (calc: object): string[] =>
|
|
108
|
+
JSON.stringify(calc)
|
|
109
|
+
.match(/"field":"[^"]+"/g)
|
|
110
|
+
?.map((match) => match.slice(9, -1)) ?? [];
|
|
111
|
+
// Calculated fields downstream of the seeds, following opaque and linear calculations.
|
|
112
|
+
const downstream = (seeds: (field: string) => boolean): Set<string> => {
|
|
113
|
+
const found = new Set<string>();
|
|
114
|
+
for (let grew = true; grew;) {
|
|
115
|
+
grew = false;
|
|
116
|
+
for (const calc of instrument.calculate) {
|
|
117
|
+
const target = `self.${calc.target}`;
|
|
118
|
+
if (found.has(target)) continue;
|
|
119
|
+
if (reads(calc).some((field) => seeds(field) || found.has(field))) {
|
|
120
|
+
found.add(target);
|
|
121
|
+
grew = true;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return found;
|
|
126
|
+
};
|
|
127
|
+
const balanceBound = downstream(
|
|
128
|
+
(field) => field.endsWith(".balance") || field.endsWith(".reserved"),
|
|
129
|
+
);
|
|
101
130
|
const equations = new Map<string, Sum>();
|
|
102
131
|
for (const field of instrument.fields)
|
|
103
|
-
if (
|
|
132
|
+
if (
|
|
133
|
+
field.type === "money" &&
|
|
134
|
+
field.value !== undefined &&
|
|
135
|
+
!assigned.has(`self.${field.name}`)
|
|
136
|
+
)
|
|
104
137
|
equations.set(
|
|
105
138
|
`self.${field.name}`,
|
|
106
139
|
new Map([["#", BigInt(field.value)]]),
|
|
@@ -146,9 +179,65 @@ export function analyzeInstrumentFinance(
|
|
|
146
179
|
}
|
|
147
180
|
return result!;
|
|
148
181
|
}
|
|
182
|
+
// A top-level create admits the snapshot it reads; an invoked child create
|
|
183
|
+
// reads its parent's live subject but keeps the parent's snapshot.
|
|
184
|
+
const invokedCreate = (document?.instruments ?? []).some((owner) =>
|
|
185
|
+
Object.values(owner.actions).some((candidate) =>
|
|
186
|
+
(candidate.invoke ?? []).some(
|
|
187
|
+
(call) =>
|
|
188
|
+
"instrument" in call &&
|
|
189
|
+
call.instrument === instrument.id &&
|
|
190
|
+
call.action === "create",
|
|
191
|
+
),
|
|
192
|
+
),
|
|
193
|
+
);
|
|
149
194
|
let invocation = 0;
|
|
150
195
|
const apply = (state: State, action: UdlAction, name: string): void => {
|
|
151
196
|
const scope = `${name}#${invocation++}`;
|
|
197
|
+
// An unknown balance becomes one opening symbol, so a whole-balance move cancels it.
|
|
198
|
+
const opening = `${scope}:opening:`;
|
|
199
|
+
for (const [account, balance] of state.balances)
|
|
200
|
+
if (balance === null)
|
|
201
|
+
state.balances.set(account, new Map([[`${opening}${account}`, 1n]]));
|
|
202
|
+
const start = new Map(state.balances);
|
|
203
|
+
// Moves read set fields after the set, so they get this invocation's symbol.
|
|
204
|
+
const changed = new Set(
|
|
205
|
+
Object.keys(action.set ?? {}).map((key) => `self.${key}`),
|
|
206
|
+
);
|
|
207
|
+
const stale = downstream((field) => changed.has(field));
|
|
208
|
+
let fresh = new Set<string>();
|
|
209
|
+
// An action that declares subject fields reads the live object, not the agreement.
|
|
210
|
+
const declared = new Set(
|
|
211
|
+
name === "create" && !invokedCreate
|
|
212
|
+
? []
|
|
213
|
+
: (action.subject?.requirements ?? []).map(
|
|
214
|
+
(entry) => `subject.${entry.field.name}`,
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
const live = new Set([
|
|
218
|
+
...declared,
|
|
219
|
+
...downstream((field) => declared.has(field)),
|
|
220
|
+
]);
|
|
221
|
+
let reading = 0;
|
|
222
|
+
const external = (field: string) =>
|
|
223
|
+
/\.(balance|reserved)$/.test(field) && !owned.has(field.slice(0, -8));
|
|
224
|
+
const resolve = (sum: Sum): Balance => {
|
|
225
|
+
let result: Balance = new Map();
|
|
226
|
+
for (const [key, coefficient] of sum) {
|
|
227
|
+
const account = key.slice(0, -8);
|
|
228
|
+
const scoped =
|
|
229
|
+
balanceBound.has(key) ||
|
|
230
|
+
fresh.has(key) ||
|
|
231
|
+
live.has(key) ||
|
|
232
|
+
external(key);
|
|
233
|
+
const term: Balance =
|
|
234
|
+
key.endsWith(".balance") && owned.has(account)
|
|
235
|
+
? (start.get(account) ?? null)
|
|
236
|
+
: new Map([[scoped ? `${scope}:${key}` : key, 1n]]);
|
|
237
|
+
result = add(result, term, coefficient);
|
|
238
|
+
}
|
|
239
|
+
return result;
|
|
240
|
+
};
|
|
152
241
|
const calculated = new Map<string, Balance>();
|
|
153
242
|
const local = new Map(
|
|
154
243
|
(action.calculate ?? []).map((c) => [`self.${c.target}`, c]),
|
|
@@ -161,6 +250,9 @@ export function analyzeInstrumentFinance(
|
|
|
161
250
|
owned.has(value.field.slice(0, -8))
|
|
162
251
|
)
|
|
163
252
|
return state.balances.get(value.field.slice(0, -8)) ?? null;
|
|
253
|
+
// Another account's balance moves between reads, so each read is its own symbol.
|
|
254
|
+
if (external(value.field))
|
|
255
|
+
return new Map([[`${scope}:${reading++}:${value.field}`, 1n]]);
|
|
164
256
|
const calculation = local.get(value.field);
|
|
165
257
|
if (calculation && !seen.has(value.field)) {
|
|
166
258
|
const next = new Set([...seen, value.field]);
|
|
@@ -177,7 +269,7 @@ export function analyzeInstrumentFinance(
|
|
|
177
269
|
return new Map([[`${scope}:${value.field}`, 1n]]);
|
|
178
270
|
}
|
|
179
271
|
}
|
|
180
|
-
return expand(raw(value, scope));
|
|
272
|
+
return resolve(expand(raw(value, scope)));
|
|
181
273
|
};
|
|
182
274
|
for (const requirement of action.requires)
|
|
183
275
|
if (requirement.kind === "compare" && requirement.operator === "==") {
|
|
@@ -217,6 +309,7 @@ export function analyzeInstrumentFinance(
|
|
|
217
309
|
),
|
|
218
310
|
);
|
|
219
311
|
};
|
|
312
|
+
fresh = new Set([...changed, ...stale]);
|
|
220
313
|
for (const move of action.moves) {
|
|
221
314
|
if ("amount" in move) {
|
|
222
315
|
const value = amount(move.amount);
|
|
@@ -240,13 +333,24 @@ export function analyzeInstrumentFinance(
|
|
|
240
333
|
state.holds.delete(move.transfer);
|
|
241
334
|
}
|
|
242
335
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
336
|
+
// Old values of set fields are gone; this invocation's values become current.
|
|
337
|
+
const settle = (sum: Balance): Balance =>
|
|
338
|
+
sum === null ||
|
|
339
|
+
[...sum.keys()].some((key) => key.startsWith(opening) || fresh.has(key))
|
|
340
|
+
? null
|
|
341
|
+
: new Map(
|
|
342
|
+
[...sum].map(([key, value]) => {
|
|
343
|
+
const field = key.slice(scope.length + 1);
|
|
344
|
+
return key.startsWith(`${scope}:`) &&
|
|
345
|
+
fresh.has(field) &&
|
|
346
|
+
!balanceBound.has(field)
|
|
347
|
+
? [field, value]
|
|
348
|
+
: [key, value];
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
for (const [account, balance] of state.balances)
|
|
352
|
+
state.balances.set(account, settle(balance));
|
|
353
|
+
for (const hold of state.holds.values()) hold.amount = settle(hold.amount);
|
|
250
354
|
};
|
|
251
355
|
const initial: State = {
|
|
252
356
|
status: instrument.lifecycle.initial,
|
package/src/object-semantics.ts
CHANGED
|
@@ -24,8 +24,9 @@ export function presentationLabel(name: string): string {
|
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
26
|
* A state is terminal when no transition lists it in `from`, so nothing can
|
|
27
|
-
* leave it.
|
|
28
|
-
*
|
|
27
|
+
* leave it. Lifecycle metadata reads this rule. The close guard and open
|
|
28
|
+
* records start from it and also drop exits a parent runs or a passed
|
|
29
|
+
* deadline has shut (the engine's `openInstanceFilter`).
|
|
29
30
|
*/
|
|
30
31
|
export function isTerminalState(
|
|
31
32
|
instrument: UdlInstrument,
|
package/src/schema.ts
CHANGED
|
@@ -258,6 +258,10 @@ const selection = z.strictObject({
|
|
|
258
258
|
window: z
|
|
259
259
|
.strictObject({ field: name, milliseconds: integer.positive() })
|
|
260
260
|
.optional(),
|
|
261
|
+
/** Rows whose half-open [start, end) period meets [from, until). */
|
|
262
|
+
overlaps: z
|
|
263
|
+
.strictObject({ start: name, end: name, from: value, until: value })
|
|
264
|
+
.optional(),
|
|
261
265
|
where: z
|
|
262
266
|
.record(path, value)
|
|
263
267
|
.refine(
|
|
@@ -318,6 +322,15 @@ export const udlCalculationSchema = z.discriminatedUnion("op", [
|
|
|
318
322
|
divisor: value,
|
|
319
323
|
rounding: z.literal("floor"),
|
|
320
324
|
}),
|
|
325
|
+
z.strictObject({
|
|
326
|
+
target: name,
|
|
327
|
+
op: z.literal("annuity"),
|
|
328
|
+
amount: value,
|
|
329
|
+
rate: value,
|
|
330
|
+
periods: value,
|
|
331
|
+
position: value,
|
|
332
|
+
part: z.enum(["principal", "interest"]),
|
|
333
|
+
}),
|
|
321
334
|
z.strictObject({
|
|
322
335
|
target: name,
|
|
323
336
|
op: z.literal("shift"),
|
package/src/validation.ts
CHANGED
|
@@ -885,6 +885,17 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
885
885
|
where,
|
|
886
886
|
"selection window requires a date field on every selected instrument",
|
|
887
887
|
);
|
|
888
|
+
if (
|
|
889
|
+
selection.overlaps &&
|
|
890
|
+
[selection.overlaps.start, selection.overlaps.end].some(
|
|
891
|
+
(name) =>
|
|
892
|
+
target.fields.find((f) => f.name === name)?.type !== "date",
|
|
893
|
+
)
|
|
894
|
+
)
|
|
895
|
+
add(
|
|
896
|
+
where,
|
|
897
|
+
"selection overlaps requires start and end date fields on every selected instrument",
|
|
898
|
+
);
|
|
888
899
|
const reference = target.fields.find(
|
|
889
900
|
(f) => f.name === selection.reference,
|
|
890
901
|
);
|
|
@@ -917,6 +928,10 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
917
928
|
} else checkValue(value, selected.type, where, input);
|
|
918
929
|
}
|
|
919
930
|
}
|
|
931
|
+
if (selection.overlaps) {
|
|
932
|
+
checkValue(selection.overlaps.from, "date", where, input);
|
|
933
|
+
checkValue(selection.overlaps.until, "date", where, input);
|
|
934
|
+
}
|
|
920
935
|
const first = targets[0];
|
|
921
936
|
if (!first) return;
|
|
922
937
|
return {
|
|
@@ -1059,6 +1074,13 @@ export function validateUdl(value: unknown): UdlValidationResult {
|
|
|
1059
1074
|
"money multiplier must be nonnegative and divisor must be positive",
|
|
1060
1075
|
);
|
|
1061
1076
|
}
|
|
1077
|
+
if (c.op === "annuity") {
|
|
1078
|
+
money(c.amount);
|
|
1079
|
+
checkValue(c.rate, "percent", where, input, act);
|
|
1080
|
+
checkValue(c.periods, "integer", where, input, act);
|
|
1081
|
+
checkValue(c.position, "integer", where, input, act);
|
|
1082
|
+
operands.push(c.rate, c.periods, c.position);
|
|
1083
|
+
}
|
|
1062
1084
|
if (c.op === "shift") {
|
|
1063
1085
|
checkValue(c.date, "date", where, input, act);
|
|
1064
1086
|
checkValue(
|