@hyperscale0/hsx 2.0.3 → 2.0.4
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 +14 -0
- package/dist/src/diagnostics.d.ts.map +1 -1
- package/dist/src/diagnostics.js +9 -0
- package/dist/src/diagnostics.js.map +1 -1
- package/dist/src/std-bundle.js +1 -1
- package/dist/src/std-bundle.js.map +1 -1
- package/dist/src/typecheck.d.ts.map +1 -1
- package/dist/src/typecheck.js +126 -5
- package/dist/src/typecheck.js.map +1 -1
- package/docs/llms-full.txt +8 -0
- package/docs/reference/diagnostics.md +8 -0
- package/package.json +3 -3
- package/src/diagnostics.ts +9 -0
- package/src/std-bundle.ts +1 -1
- package/src/typecheck.ts +170 -5
- package/std/money_flows/held_payment.hsx +23 -10
package/src/typecheck.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deriveUdlActionEffects,
|
|
3
3
|
udlClauseVocabulary,
|
|
4
|
+
type UdlAction,
|
|
4
5
|
type UdlDocument,
|
|
5
6
|
type UdlInstrument,
|
|
6
7
|
} from "@hyperscale0/udl";
|
|
@@ -54,6 +55,7 @@ type ClauseDefinition = (typeof udlClauseVocabulary)[number];
|
|
|
54
55
|
interface ConcreteInstrument {
|
|
55
56
|
readonly aliases: ReadonlyMap<string, Expr>;
|
|
56
57
|
readonly body: BlockExpr;
|
|
58
|
+
readonly callee?: string;
|
|
57
59
|
readonly generatedPrefix: boolean;
|
|
58
60
|
readonly name: IdentExpr;
|
|
59
61
|
readonly parties: ReadonlySet<string>;
|
|
@@ -350,6 +352,7 @@ export function checkGeneralProgram(
|
|
|
350
352
|
).map((candidate) => ({
|
|
351
353
|
...candidate,
|
|
352
354
|
aliases: scoped.aliases,
|
|
355
|
+
callee: candidate.name.name,
|
|
353
356
|
parties: scoped.parties,
|
|
354
357
|
})),
|
|
355
358
|
);
|
|
@@ -397,11 +400,21 @@ export function checkGeneralProgram(
|
|
|
397
400
|
constructedInstruments(decl.name, merged, diagnostics),
|
|
398
401
|
boundPorts,
|
|
399
402
|
applicationPorts,
|
|
400
|
-
).map((candidate) =>
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
403
|
+
).map((candidate) => {
|
|
404
|
+
let candidateCallee = callee;
|
|
405
|
+
if (
|
|
406
|
+
candidate.name.name !== decl.name.name &&
|
|
407
|
+
candidate.name.name.startsWith(`${decl.name.name}_`)
|
|
408
|
+
) {
|
|
409
|
+
candidateCallee = `${callee}${candidate.name.name.slice(decl.name.name.length)}`;
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
...candidate,
|
|
413
|
+
aliases: scoped.aliases,
|
|
414
|
+
callee: candidateCallee,
|
|
415
|
+
parties: scoped.parties,
|
|
416
|
+
};
|
|
417
|
+
}),
|
|
405
418
|
);
|
|
406
419
|
for (const port of boundPorts.values()) {
|
|
407
420
|
if (applicationPorts.has(port)) reachedPorts.add(port);
|
|
@@ -422,6 +435,15 @@ export function checkGeneralProgram(
|
|
|
422
435
|
|
|
423
436
|
const allocated = allocateGeneratedPrefixes(concrete);
|
|
424
437
|
|
|
438
|
+
if (options.publishedCatalog) {
|
|
439
|
+
checkPortCaptureTypes(
|
|
440
|
+
allocated,
|
|
441
|
+
options.publishedCatalog,
|
|
442
|
+
aliases,
|
|
443
|
+
diagnostics,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
425
447
|
const instruments: TypedInstrument[] = [];
|
|
426
448
|
const ids = new Set<string>();
|
|
427
449
|
for (const candidate of allocated) {
|
|
@@ -913,6 +935,149 @@ function publishedFieldType(
|
|
|
913
935
|
return target ? { kind: "ref", target: target.id } : { kind: "text" };
|
|
914
936
|
}
|
|
915
937
|
|
|
938
|
+
const STRING_BACKED_PORT_KINDS: ReadonlySet<HsxType["kind"]> = new Set([
|
|
939
|
+
"account",
|
|
940
|
+
"condition",
|
|
941
|
+
"date",
|
|
942
|
+
"party",
|
|
943
|
+
"ref",
|
|
944
|
+
"text",
|
|
945
|
+
]);
|
|
946
|
+
const INTEGER_BACKED_PORT_KINDS: ReadonlySet<HsxType["kind"]> = new Set([
|
|
947
|
+
"bps",
|
|
948
|
+
"integer",
|
|
949
|
+
"percent",
|
|
950
|
+
]);
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* A published schema keeps less than the HSX type: dates and accounts publish
|
|
954
|
+
* as plain strings, percents and bps as integers. Compare by the family the
|
|
955
|
+
* schema can still express so the check never rejects a richer declared type.
|
|
956
|
+
*/
|
|
957
|
+
function samePortType(declared: HsxType, captured: HsxType): boolean {
|
|
958
|
+
if (captured.kind === "unknown" || declared.kind === "unknown") return true;
|
|
959
|
+
if (captured.kind === "text")
|
|
960
|
+
return STRING_BACKED_PORT_KINDS.has(declared.kind);
|
|
961
|
+
if (captured.kind === "integer") {
|
|
962
|
+
return INTEGER_BACKED_PORT_KINDS.has(declared.kind);
|
|
963
|
+
}
|
|
964
|
+
if (declared.kind !== captured.kind) return false;
|
|
965
|
+
if (captured.currency && declared.currency !== captured.currency) {
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
if (captured.target && declared.target !== captured.target) {
|
|
969
|
+
return false;
|
|
970
|
+
}
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function portTypeWords(type: HsxType): string {
|
|
975
|
+
if (type.kind === "ref" && type.target) return `ref<${type.target}>`;
|
|
976
|
+
return typeWords(type);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function getFieldSchemaFromAction(
|
|
980
|
+
action: UdlAction,
|
|
981
|
+
fieldName: string,
|
|
982
|
+
): Readonly<Record<string, JsonValue>> | undefined {
|
|
983
|
+
if (!action.captureInput) return undefined;
|
|
984
|
+
const properties = action.input?.properties as
|
|
985
|
+
| Record<string, Record<string, JsonValue>>
|
|
986
|
+
| undefined;
|
|
987
|
+
if (!properties) return undefined;
|
|
988
|
+
|
|
989
|
+
for (const inputKey of Object.values(action.captureInput)) {
|
|
990
|
+
if (inputKey !== fieldName && camel(inputKey) !== camel(fieldName)) {
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const schema = properties[inputKey] ?? properties[camel(inputKey)];
|
|
994
|
+
if (schema && typeof schema === "object") return schema;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
return undefined;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function findCapturedFieldSchema(
|
|
1001
|
+
catalogInstrument: UdlInstrument,
|
|
1002
|
+
actionName: string,
|
|
1003
|
+
fieldName: string,
|
|
1004
|
+
): Readonly<Record<string, JsonValue>> | undefined {
|
|
1005
|
+
const targetAction = catalogInstrument.actions[actionName];
|
|
1006
|
+
return targetAction
|
|
1007
|
+
? getFieldSchemaFromAction(targetAction, fieldName)
|
|
1008
|
+
: undefined;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function checkPortCaptureTypes(
|
|
1012
|
+
candidates: readonly ConcreteInstrument[],
|
|
1013
|
+
publishedCatalog: UdlDocument,
|
|
1014
|
+
topLevelAliases: ReadonlyMap<string, Expr>,
|
|
1015
|
+
diagnostics: GeneralDiagnostic[],
|
|
1016
|
+
): void {
|
|
1017
|
+
const catalogById = new Map<string, UdlInstrument>(
|
|
1018
|
+
publishedCatalog.instruments.map((instrument) => [
|
|
1019
|
+
instrument.id,
|
|
1020
|
+
instrument,
|
|
1021
|
+
]),
|
|
1022
|
+
);
|
|
1023
|
+
|
|
1024
|
+
for (const candidate of candidates) {
|
|
1025
|
+
const catalogInstrument =
|
|
1026
|
+
catalogById.get(candidate.name.name) ??
|
|
1027
|
+
(candidate.callee ? catalogById.get(candidate.callee) : undefined);
|
|
1028
|
+
if (!catalogInstrument) continue;
|
|
1029
|
+
|
|
1030
|
+
const aliases = new Map([...topLevelAliases, ...candidate.aliases]);
|
|
1031
|
+
|
|
1032
|
+
for (const [actionName, declaredPorts] of candidate.ports) {
|
|
1033
|
+
for (const port of declaredPorts) {
|
|
1034
|
+
const rawShape = entry(port.body, "shape")?.value;
|
|
1035
|
+
if (rawShape?.kind !== "block") continue;
|
|
1036
|
+
|
|
1037
|
+
for (const fieldEntry of rawShape.entries) {
|
|
1038
|
+
const fieldName = fieldEntry.key.name;
|
|
1039
|
+
const capturedSchema = findCapturedFieldSchema(
|
|
1040
|
+
catalogInstrument,
|
|
1041
|
+
actionName,
|
|
1042
|
+
fieldName,
|
|
1043
|
+
);
|
|
1044
|
+
if (!capturedSchema) continue;
|
|
1045
|
+
|
|
1046
|
+
const capturedType = publishedFieldType(
|
|
1047
|
+
capturedSchema,
|
|
1048
|
+
publishedCatalog.instruments,
|
|
1049
|
+
);
|
|
1050
|
+
const declaredType = typeOf(fieldEntry.value, aliases);
|
|
1051
|
+
|
|
1052
|
+
if (!samePortType(declaredType, capturedType)) {
|
|
1053
|
+
const declaredWords = portTypeWords(declaredType);
|
|
1054
|
+
const capturedWords = portTypeWords(capturedType);
|
|
1055
|
+
const message = `decision port ${port.name.name} field ${fieldName} declares ${declaredWords} but instrument ${catalogInstrument.id} captures it as ${capturedWords}`;
|
|
1056
|
+
const fix = `declare ${fieldName} as ${capturedWords} in decision port ${port.name.name}`;
|
|
1057
|
+
if (
|
|
1058
|
+
!diagnostics.some(
|
|
1059
|
+
(prior) =>
|
|
1060
|
+
prior.code === "HSX1026" &&
|
|
1061
|
+
prior.span.start === fieldEntry.value.span.start &&
|
|
1062
|
+
prior.span.end === fieldEntry.value.span.end &&
|
|
1063
|
+
prior.message === message,
|
|
1064
|
+
)
|
|
1065
|
+
) {
|
|
1066
|
+
diagnostics.push({
|
|
1067
|
+
code: "HSX1026",
|
|
1068
|
+
fix,
|
|
1069
|
+
message,
|
|
1070
|
+
severity: "error",
|
|
1071
|
+
span: fieldEntry.value.span,
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
|
|
916
1081
|
function typedPublishedSubject(
|
|
917
1082
|
subject: UdlDocument["subjects"][number],
|
|
918
1083
|
origin: Span,
|
|
@@ -39,6 +39,7 @@ module std.money_flows.held_payment
|
|
|
39
39
|
// - `cancel_charge_bps`: Optional cancellation charge in basis points. Declaring it gives the settlement a
|
|
40
40
|
// quoted cancellation: `quote_cancellation` prices the charge and the refund and freezes both,
|
|
41
41
|
// `cancel` pays the refund to the payer, and `retain_cancellation_charge` pays the charge to the payee.
|
|
42
|
+
// A zero charge keeps the flow with a zero fee.
|
|
42
43
|
// - `cancel_offer_life`: ISO 8601 duration a cancellation quote stays open, required with `cancel_charge_bps`.
|
|
43
44
|
//
|
|
44
45
|
// ### Decision ports
|
|
@@ -70,6 +71,8 @@ export instrument held_payment<C>(payer: party, payee: party, amount: money<C>,
|
|
|
70
71
|
let(payee_fee): get(fees, payee);
|
|
71
72
|
let(payer_fee_kind): kind(payer_fee);
|
|
72
73
|
let(payee_fee_kind): kind(payee_fee);
|
|
74
|
+
let(has_cancel_quote): if_eq(kind(cancel_charge_bps), "boolean", false, true);
|
|
75
|
+
let(has_charge): if_eq(cancel_charge_bps, 0, false, true);
|
|
73
76
|
when_eq(payee_fee_kind, binding) {
|
|
74
77
|
when(on_cancel) {
|
|
75
78
|
unsupported {
|
|
@@ -79,7 +82,7 @@ export instrument held_payment<C>(payer: party, payee: party, amount: money<C>,
|
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
}
|
|
82
|
-
when(
|
|
85
|
+
when(has_cancel_quote) {
|
|
83
86
|
when(on_cancel) {
|
|
84
87
|
unsupported {
|
|
85
88
|
code: HSX1110;
|
|
@@ -110,10 +113,10 @@ export instrument held_payment<C>(payer: party, payee: party, amount: money<C>,
|
|
|
110
113
|
}
|
|
111
114
|
}
|
|
112
115
|
when(cancel_offer_life) {
|
|
113
|
-
when_not(
|
|
116
|
+
when_not(has_cancel_quote) {
|
|
114
117
|
unsupported {
|
|
115
118
|
code: HSX1110;
|
|
116
|
-
message: "cancel_offer_life needs
|
|
119
|
+
message: "cancel_offer_life needs cancel_charge_bps to quote";
|
|
117
120
|
fix: "declare the cancellation charge in basis points, or drop cancel_offer_life";
|
|
118
121
|
}
|
|
119
122
|
}
|
|
@@ -202,7 +205,7 @@ export instrument held_payment<C>(payer: party, payee: party, amount: money<C>,
|
|
|
202
205
|
when_not(cancel_splits) {
|
|
203
206
|
states created funded disputed released;
|
|
204
207
|
when(on_cancel) { states cancelled; }
|
|
205
|
-
when(
|
|
208
|
+
when(has_cancel_quote) { states cancellation_quoted cancelled settled; }
|
|
206
209
|
states abandoned;
|
|
207
210
|
}
|
|
208
211
|
when(cancel_splits) { states created funding_1 funded disputed releasing_1 released cancelling_1 cancelled abandoned; }
|
|
@@ -219,14 +222,14 @@ export instrument held_payment<C>(payer: party, payee: party, amount: money<C>,
|
|
|
219
222
|
when_not(payee_fee) {
|
|
220
223
|
when_not(cancel_splits) {
|
|
221
224
|
on fund_piece_1: created -> funded;
|
|
222
|
-
when_not(
|
|
225
|
+
when_not(has_cancel_quote) {
|
|
223
226
|
on [release_name]: funded -> released;
|
|
224
227
|
when(release_deadline) { on release_on_deadline: funded -> released; }
|
|
225
228
|
}
|
|
226
|
-
when(
|
|
229
|
+
when(has_cancel_quote) {
|
|
227
230
|
on [release_name]: funded | cancellation_quoted -> released;
|
|
228
231
|
when(release_deadline) { on release_on_deadline: funded | cancellation_quoted -> released; }
|
|
229
|
-
on quote_cancellation: funded -> cancellation_quoted;
|
|
232
|
+
on quote_cancellation: funded | cancellation_quoted -> cancellation_quoted;
|
|
230
233
|
on cancel: cancellation_quoted -> cancelled;
|
|
231
234
|
on retain_cancellation_charge: cancelled -> settled;
|
|
232
235
|
}
|
|
@@ -275,7 +278,12 @@ export instrument held_payment<C>(payer: party, payee: party, amount: money<C>,
|
|
|
275
278
|
on unfund_piece_1: funding_1 | abandoning_1 -> abandoned;
|
|
276
279
|
}
|
|
277
280
|
}
|
|
278
|
-
|
|
281
|
+
when_not(has_cancel_quote) {
|
|
282
|
+
on dispute: funded -> disputed;
|
|
283
|
+
}
|
|
284
|
+
when(has_cancel_quote) {
|
|
285
|
+
on dispute: funded | cancellation_quoted -> disputed;
|
|
286
|
+
}
|
|
279
287
|
on resume: disputed -> funded;
|
|
280
288
|
on abandon: created -> abandoned;
|
|
281
289
|
}
|
|
@@ -520,7 +528,7 @@ export instrument held_payment<C>(payer: party, payee: party, amount: money<C>,
|
|
|
520
528
|
steps: [];
|
|
521
529
|
}
|
|
522
530
|
}
|
|
523
|
-
when(
|
|
531
|
+
when(has_cancel_quote) {
|
|
524
532
|
action quote_cancellation {
|
|
525
533
|
agent_description: concat("Price the cancellation and hold that price open. No money moves. The charge the ", words(payee), " keeps and the refund the ", words(payer), " gets back are worked out here and frozen for ", cancel_offer_life, ", after which a fresh call prices it again. Call cancel to spend the quote.");
|
|
526
534
|
summary: "Price the cancellation and hold that price open";
|
|
@@ -554,7 +562,12 @@ export instrument held_payment<C>(payer: party, payee: party, amount: money<C>,
|
|
|
554
562
|
let(metadata_phase): "metadata.phase";
|
|
555
563
|
agent_description: concat("Pay the quoted cancellation charge out of escrow to the ", words(payee), ". This moves money and does not reverse. The settlement must already be cancelled, so the figure is the one the quote fixed and the escrow ends empty.");
|
|
556
564
|
summary: concat("Pay the quoted cancellation charge to the ", words(payee));
|
|
557
|
-
|
|
565
|
+
when(has_charge) {
|
|
566
|
+
moves: [{ key: charge; operation: internal_transfer.create; bind: { amount: { from: instance; path: refs.cancellationChargeAmount; }; currency: { from: instance; path: fields.currency; }; destinationAccountId: { from: instance; path: concat("fields.", payee_account_field); }; [metadata_instrument]: { from: const; value: instrument; }; [metadata_instance]: { from: instance; path: instrumentInstanceId; }; [metadata_phase]: { from: const; value: retain_cancellation_charge; }; productId: { from: instance; path: productId; }; sourceAccountId: { from: instance; path: refs.escrowAccountId; }; }; capture: { retainCancellationChargeTransferId: transferId; }; }];
|
|
567
|
+
}
|
|
568
|
+
when_not(has_charge) {
|
|
569
|
+
moves: [];
|
|
570
|
+
}
|
|
558
571
|
steps: [];
|
|
559
572
|
}
|
|
560
573
|
}
|