@hyperscale0/hsx 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 +58 -9
- package/LICENSING.md +3 -3
- package/README.md +39 -149
- package/TRADEMARKS.md +4 -4
- package/bin/hsx.ts +2 -0
- package/dist/bin/hsx.js +2 -0
- package/dist/bin/hsx.js.map +1 -1
- package/dist/src/ast.d.ts +135 -10
- package/dist/src/ast.d.ts.map +1 -1
- package/dist/src/ast.js +24 -0
- package/dist/src/ast.js.map +1 -1
- package/dist/src/cli.d.ts +4 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +172 -16
- package/dist/src/cli.js.map +1 -1
- package/dist/src/compile.d.ts +42 -7
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +82 -16
- package/dist/src/compile.js.map +1 -1
- package/dist/src/cost.d.ts +78 -0
- package/dist/src/cost.d.ts.map +1 -0
- package/dist/src/cost.js +369 -0
- package/dist/src/cost.js.map +1 -0
- package/dist/src/diagnostics.d.ts +11 -0
- package/dist/src/diagnostics.d.ts.map +1 -0
- package/dist/src/diagnostics.js +420 -0
- package/dist/src/diagnostics.js.map +1 -0
- package/dist/src/emit.d.ts +34 -0
- package/dist/src/emit.d.ts.map +1 -0
- package/dist/src/emit.js +137 -0
- package/dist/src/emit.js.map +1 -0
- package/dist/src/entry-overrides.d.ts +17 -3
- package/dist/src/entry-overrides.d.ts.map +1 -1
- package/dist/src/entry-overrides.js +180 -35
- package/dist/src/entry-overrides.js.map +1 -1
- package/dist/src/format.d.ts +10 -0
- package/dist/src/format.d.ts.map +1 -0
- package/dist/src/format.js +183 -0
- package/dist/src/format.js.map +1 -0
- package/dist/src/index.d.ts +11 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +10 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/ir.d.ts +66 -0
- package/dist/src/ir.d.ts.map +1 -0
- package/dist/src/ir.js +15 -0
- package/dist/src/ir.js.map +1 -0
- package/dist/src/lex.d.ts +9 -1
- package/dist/src/lex.d.ts.map +1 -1
- package/dist/src/lex.js +50 -4
- package/dist/src/lex.js.map +1 -1
- package/dist/src/limits.d.ts +2 -3
- package/dist/src/limits.d.ts.map +1 -1
- package/dist/src/limits.js +2 -3
- package/dist/src/limits.js.map +1 -1
- package/dist/src/lsp/server.d.ts +32 -0
- package/dist/src/lsp/server.d.ts.map +1 -0
- package/dist/src/lsp/server.js +391 -0
- package/dist/src/lsp/server.js.map +1 -0
- package/dist/src/modules.d.ts +38 -0
- package/dist/src/modules.d.ts.map +1 -0
- package/dist/src/modules.js +368 -0
- package/dist/src/modules.js.map +1 -0
- package/dist/src/parse.d.ts.map +1 -1
- package/dist/src/parse.js +590 -17
- package/dist/src/parse.js.map +1 -1
- package/dist/src/std-bundle.d.ts +2 -0
- package/dist/src/std-bundle.d.ts.map +1 -0
- package/dist/src/std-bundle.js +88 -0
- package/dist/src/std-bundle.js.map +1 -0
- package/dist/src/std-library.d.ts +5 -0
- package/dist/src/std-library.d.ts.map +1 -0
- package/dist/src/std-library.js +9 -0
- package/dist/src/std-library.js.map +1 -0
- package/dist/src/typecheck.d.ts +8 -0
- package/dist/src/typecheck.d.ts.map +1 -0
- package/dist/src/typecheck.js +2958 -0
- package/dist/src/typecheck.js.map +1 -0
- package/dist/src/version.d.ts +4 -15
- package/dist/src/version.d.ts.map +1 -1
- package/dist/src/version.js +4 -15
- package/dist/src/version.js.map +1 -1
- package/docs/README.md +26 -0
- package/docs/guide/01-first-program.md +24 -0
- package/docs/guide/02-money.md +25 -0
- package/docs/guide/03-instruments.md +22 -0
- package/docs/guide/04-lifecycles.md +25 -0
- package/docs/guide/05-fees-and-splits.md +45 -0
- package/docs/guide/06-schedules.md +22 -0
- package/docs/guide/07-composition.md +46 -0
- package/docs/guide/08-writing-a-module.md +26 -0
- package/docs/guide/09-cost.md +24 -0
- package/docs/guide/10-diagnostics.md +27 -0
- package/docs/llms-full.txt +1739 -0
- package/docs/llms.txt +45 -0
- package/docs/playground.md +54 -0
- package/docs/reference/cli.md +35 -0
- package/docs/reference/diagnostics.md +619 -0
- package/docs/reference/grammar.md +39 -0
- package/docs/reference/std/advance.md +35 -0
- package/docs/reference/std/cancellable_booking.md +35 -0
- package/docs/reference/std/captured_payment.md +43 -0
- package/docs/reference/std/conditional_disbursement.md +33 -0
- package/docs/reference/std/credit_facility.md +35 -0
- package/docs/reference/std/held_payment.md +64 -0
- package/docs/reference/std/instant_transfer.md +30 -0
- package/docs/reference/std/metered.md +27 -0
- package/docs/reference/std/pooled_split.md +26 -0
- package/docs/reference/std/premium_forward.md +39 -0
- package/docs/reference/std/reconciled_payout.md +33 -0
- package/docs/reference/std/recurring_collection.md +25 -0
- package/docs/reference/std/rotating_pool.md +42 -0
- package/docs/reference/std/scheduled.md +51 -0
- package/docs/reference/std/security_deposit.md +46 -0
- package/docs/reference/std/settlement_batch.md +42 -0
- package/docs/reference/std/swap.md +46 -0
- package/docs/reference/std/threshold_pool.md +47 -0
- package/docs/reference/std/weighted_distribution.md +39 -0
- package/docs/reference/types.md +20 -0
- package/docs/reference/udl-output.md +13 -0
- package/docs/sessions/2026-09-02-two-instruments.hsx +21 -0
- package/docs/sessions/2026-09-02-two-instruments.md +41 -0
- package/examples/01-first-program/README.md +45 -0
- package/examples/01-first-program/tip-jar.hsx +17 -0
- package/examples/02-imports-and-archetypes/README.md +96 -0
- package/examples/02-imports-and-archetypes/photo-booth.hsx +37 -0
- package/examples/03-diagnostics/README.md +68 -0
- package/examples/03-diagnostics/corner-shop-fixed.hsx +22 -0
- package/examples/03-diagnostics/corner-shop.hsx +19 -0
- package/examples/04-complete-product/README.md +84 -0
- package/examples/04-complete-product/study-hall.hsx +67 -0
- package/examples/05-watch-club/README.md +9 -0
- package/examples/05-watch-club/watch-club.hsx +124 -0
- package/examples/README.md +18 -0
- package/examples/advance/README.md +3 -0
- package/examples/advance/advance.hsx +13 -0
- package/examples/advance/advance.udl +339 -0
- package/examples/cancellable_booking/README.md +3 -0
- package/examples/cancellable_booking/cancellable_booking.hsx +14 -0
- package/examples/cancellable_booking/cancellable_booking.udl +391 -0
- package/examples/captured_payment/README.md +3 -0
- package/examples/captured_payment/captured_payment.hsx +21 -0
- package/examples/captured_payment/captured_payment.udl +677 -0
- package/examples/conditional_disbursement/README.md +3 -0
- package/examples/conditional_disbursement/conditional_disbursement.hsx +17 -0
- package/examples/conditional_disbursement/conditional_disbursement.udl +412 -0
- package/examples/cost-table.json +386 -0
- package/examples/credit_facility/README.md +3 -0
- package/examples/credit_facility/credit_facility.hsx +27 -0
- package/examples/credit_facility/credit_facility.udl +1373 -0
- package/examples/held_payment/README.md +3 -0
- package/examples/held_payment/held_payment.hsx +13 -0
- package/examples/held_payment/held_payment.udl +507 -0
- package/examples/instant_transfer/README.md +3 -0
- package/examples/instant_transfer/instant_transfer.hsx +16 -0
- package/examples/instant_transfer/instant_transfer.udl +364 -0
- package/examples/metered/README.md +3 -0
- package/examples/metered/metered.hsx +13 -0
- package/examples/metered/metered.udl +247 -0
- package/examples/pooled_split/README.md +3 -0
- package/examples/pooled_split/pooled_split.hsx +15 -0
- package/examples/pooled_split/pooled_split.udl +425 -0
- package/examples/premium_forward/README.md +3 -0
- package/examples/premium_forward/premium_forward.hsx +19 -0
- package/examples/premium_forward/premium_forward.udl +594 -0
- package/examples/reconciled_payout/README.md +3 -0
- package/examples/reconciled_payout/reconciled_payout.hsx +13 -0
- package/examples/reconciled_payout/reconciled_payout.udl +303 -0
- package/examples/recurring_collection/README.md +3 -0
- package/examples/recurring_collection/recurring_collection.hsx +21 -0
- package/examples/recurring_collection/recurring_collection.udl +1130 -0
- package/examples/rotating_pool/README.md +3 -0
- package/examples/rotating_pool/rotating_pool.hsx +18 -0
- package/examples/rotating_pool/rotating_pool.udl +4352 -0
- package/examples/scheduled/README.md +3 -0
- package/examples/scheduled/scheduled.hsx +12 -0
- package/examples/scheduled/scheduled.udl +335 -0
- package/examples/security_deposit/README.md +3 -0
- package/examples/security_deposit/security_deposit.hsx +21 -0
- package/examples/security_deposit/security_deposit.udl +293 -0
- package/examples/settlement_batch/README.md +3 -0
- package/examples/settlement_batch/settlement_batch.hsx +19 -0
- package/examples/settlement_batch/settlement_batch.udl +753 -0
- package/examples/swap/README.md +3 -0
- package/examples/swap/swap.hsx +19 -0
- package/examples/swap/swap.udl +876 -0
- package/examples/threshold_pool/README.md +3 -0
- package/examples/threshold_pool/threshold_pool.hsx +16 -0
- package/examples/threshold_pool/threshold_pool.udl +667 -0
- package/examples/weighted_distribution/README.md +3 -0
- package/examples/weighted_distribution/weighted_distribution.hsx +20 -0
- package/examples/weighted_distribution/weighted_distribution.udl +337 -0
- package/package.json +27 -6
- package/skills/hsx/SKILL.md +483 -0
- package/src/ast.ts +193 -9
- package/src/cli.ts +191 -20
- package/src/compile.ts +160 -23
- package/src/cost.ts +579 -0
- package/src/diagnostics.ts +442 -0
- package/src/emit.ts +201 -0
- package/src/entry-overrides.ts +263 -48
- package/src/format.ts +261 -0
- package/src/index.ts +34 -4
- package/src/ir.ts +99 -0
- package/src/lex.ts +59 -4
- package/src/limits.ts +2 -3
- package/src/lsp/server.ts +460 -0
- package/src/modules.ts +446 -0
- package/src/parse.ts +655 -21
- package/src/std-bundle.ts +91 -0
- package/src/std-library.ts +12 -0
- package/src/typecheck.ts +3876 -0
- package/src/version.ts +4 -16
- package/std/SEMANTICS.md +130 -0
- package/std/settlements/advance.hsx +177 -0
- package/std/settlements/cancellable_booking.hsx +87 -0
- package/std/settlements/captured_payment.hsx +144 -0
- package/std/settlements/conditional_disbursement.hsx +185 -0
- package/std/settlements/credit_facility.hsx +167 -0
- package/std/settlements/held_payment.hsx +578 -0
- package/std/settlements/index.hsx +3 -0
- package/std/settlements/instant_transfer.hsx +190 -0
- package/std/settlements/metered.hsx +67 -0
- package/std/settlements/pooled_split.hsx +68 -0
- package/std/settlements/premium_forward.hsx +257 -0
- package/std/settlements/reconciled_payout.hsx +77 -0
- package/std/settlements/recurring_collection.hsx +27 -0
- package/std/settlements/rotating_pool.hsx +252 -0
- package/std/settlements/scheduled.hsx +641 -0
- package/std/settlements/security_deposit.hsx +166 -0
- package/std/settlements/settlement_batch.hsx +122 -0
- package/std/settlements/swap.hsx +1114 -0
- package/std/settlements/threshold_pool.hsx +221 -0
- package/std/settlements/weighted_distribution.hsx +141 -0
- package/dist/src/archetypes.d.ts +0 -17
- package/dist/src/archetypes.d.ts.map +0 -1
- package/dist/src/archetypes.js +0 -327
- package/dist/src/archetypes.js.map +0 -1
- package/dist/src/check.d.ts +0 -12
- package/dist/src/check.d.ts.map +0 -1
- package/dist/src/check.js +0 -1910
- package/dist/src/check.js.map +0 -1
- package/dist/src/lower.d.ts +0 -146
- package/dist/src/lower.d.ts.map +0 -1
- package/dist/src/lower.js +0 -4570
- package/dist/src/lower.js.map +0 -1
- package/dist/src/model.d.ts +0 -467
- package/dist/src/model.d.ts.map +0 -1
- package/dist/src/model.js +0 -15
- package/dist/src/model.js.map +0 -1
- package/spec/hsx-ir.schema.json +0 -920
- package/src/archetypes.ts +0 -340
- package/src/check.ts +0 -2938
- package/src/lower.ts +0 -5416
- package/src/model.ts +0 -519
package/src/cost.ts
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import {
|
|
2
|
+
udlEffectKinds,
|
|
3
|
+
type UdlDocument,
|
|
4
|
+
type UdlEffectKind,
|
|
5
|
+
} from "@hyperscale0/udl";
|
|
6
|
+
import type { GeneralDiagnostic, JsonValue, TypedProgram } from "./ir.ts";
|
|
7
|
+
|
|
8
|
+
export type HsxEffectKind = UdlEffectKind;
|
|
9
|
+
|
|
10
|
+
export interface UdlCostTable {
|
|
11
|
+
readonly version: string;
|
|
12
|
+
/** Digest of the effective prices and fixed anchors after overrides. */
|
|
13
|
+
readonly effectiveTableDigest: string;
|
|
14
|
+
readonly currency: string;
|
|
15
|
+
/** Billable runtime meters the platform's operation contract can emit. */
|
|
16
|
+
readonly declaredMeters: readonly string[];
|
|
17
|
+
readonly rows: readonly {
|
|
18
|
+
readonly signature: string;
|
|
19
|
+
readonly perEventMinor: string;
|
|
20
|
+
readonly bps: number;
|
|
21
|
+
readonly payer: "product" | "end_customer";
|
|
22
|
+
/** Collection policy. Runtime resolves movement rows to execution or invoice. */
|
|
23
|
+
readonly settlement: "per_event" | "invoice";
|
|
24
|
+
readonly meter: string;
|
|
25
|
+
readonly volumeMeter?: string;
|
|
26
|
+
}[];
|
|
27
|
+
readonly fixed: {
|
|
28
|
+
readonly weights: Readonly<Record<string, number>>;
|
|
29
|
+
readonly monthlyBaseMinor: string;
|
|
30
|
+
readonly monthlyPerPointMinor: string;
|
|
31
|
+
readonly activationBaseMinor: string;
|
|
32
|
+
readonly activationPerPointMinor: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface UdlCostManifest {
|
|
37
|
+
readonly costTableVersion: string;
|
|
38
|
+
readonly effectiveTableDigest: string;
|
|
39
|
+
readonly currency: string;
|
|
40
|
+
/** Complete runtime meter allowlist, independent of clause effects and payer. */
|
|
41
|
+
readonly declaredMeters: readonly string[];
|
|
42
|
+
readonly fixed: {
|
|
43
|
+
readonly complexityScore: number;
|
|
44
|
+
readonly monthlyMinor: string;
|
|
45
|
+
readonly activationMinor: string;
|
|
46
|
+
readonly components: readonly {
|
|
47
|
+
readonly weight: string;
|
|
48
|
+
readonly count: number;
|
|
49
|
+
}[];
|
|
50
|
+
};
|
|
51
|
+
readonly actions: readonly {
|
|
52
|
+
readonly instrument: string;
|
|
53
|
+
readonly action: string;
|
|
54
|
+
readonly components: readonly {
|
|
55
|
+
readonly signature: string;
|
|
56
|
+
readonly count: number;
|
|
57
|
+
readonly perEventMinor: string;
|
|
58
|
+
readonly bps: number;
|
|
59
|
+
readonly payer: "product" | "end_customer";
|
|
60
|
+
readonly settlement: "per_event" | "invoice";
|
|
61
|
+
readonly meter: string;
|
|
62
|
+
readonly volumeMeter?: string;
|
|
63
|
+
}[];
|
|
64
|
+
readonly perEventMinor: string;
|
|
65
|
+
readonly endCustomerPerEventMinor: string;
|
|
66
|
+
}[];
|
|
67
|
+
/** Frozen list prices for every declared runtime meter. */
|
|
68
|
+
readonly meterPrices: readonly UdlCostTable["rows"][number][];
|
|
69
|
+
readonly monthlyEstimate: {
|
|
70
|
+
readonly expression: string;
|
|
71
|
+
readonly variables: readonly string[];
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export type CostManifestResult =
|
|
76
|
+
| { readonly diagnostics: readonly GeneralDiagnostic[]; readonly ok: false }
|
|
77
|
+
| { readonly manifest: UdlCostManifest; readonly ok: true };
|
|
78
|
+
|
|
79
|
+
export function buildCostManifest(
|
|
80
|
+
program: TypedProgram,
|
|
81
|
+
table: UdlCostTable | undefined,
|
|
82
|
+
composesCatalogBlueprint: boolean,
|
|
83
|
+
): CostManifestResult {
|
|
84
|
+
if (!table) {
|
|
85
|
+
return {
|
|
86
|
+
diagnostics: [
|
|
87
|
+
{
|
|
88
|
+
code: "HSX1301",
|
|
89
|
+
fix: "supply the versioned UDL cost table to the compiler",
|
|
90
|
+
message:
|
|
91
|
+
"the compiler cannot price this program without a cost table",
|
|
92
|
+
severity: "error",
|
|
93
|
+
span: program.origin,
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
ok: false,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!/^[A-Z]{3}$/.test(table.currency)) {
|
|
101
|
+
return {
|
|
102
|
+
diagnostics: [
|
|
103
|
+
{
|
|
104
|
+
code: "HSX1303",
|
|
105
|
+
fix: "name one three-letter uppercase billing currency on the cost table",
|
|
106
|
+
message: `cost table ${table.version} has invalid billing currency ${table.currency}`,
|
|
107
|
+
severity: "error",
|
|
108
|
+
span: program.origin,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
ok: false,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const diagnostics: GeneralDiagnostic[] = [];
|
|
116
|
+
const rowsBySignature = new Map<string, UdlCostTable["rows"]>();
|
|
117
|
+
for (const row of table.rows) {
|
|
118
|
+
rowsBySignature.set(row.signature, [
|
|
119
|
+
...(rowsBySignature.get(row.signature) ?? []),
|
|
120
|
+
row,
|
|
121
|
+
]);
|
|
122
|
+
}
|
|
123
|
+
const actions: UdlCostManifest["actions"][number][] = [];
|
|
124
|
+
for (const instrument of program.instruments) {
|
|
125
|
+
for (const action of instrument.actions) {
|
|
126
|
+
const components: UdlCostManifest["actions"][number]["components"][number][] =
|
|
127
|
+
[];
|
|
128
|
+
let productPerEvent = 0n;
|
|
129
|
+
let endCustomerPerEvent = 0n;
|
|
130
|
+
for (const [signature, count] of effectCounts(action.effects)) {
|
|
131
|
+
const rows = rowsBySignature.get(signature) ?? [];
|
|
132
|
+
if (rows.length === 0) {
|
|
133
|
+
diagnostics.push({
|
|
134
|
+
code: "HSX1301",
|
|
135
|
+
fix: `add ${signature} to cost table ${table.version}`,
|
|
136
|
+
message: `${instrument.id}.${action.name} has unpriced effect signature ${signature}`,
|
|
137
|
+
severity: "error",
|
|
138
|
+
span: action.origin,
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
for (const row of rows) {
|
|
143
|
+
if (!validPrice(row)) {
|
|
144
|
+
diagnostics.push({
|
|
145
|
+
code: "HSX1302",
|
|
146
|
+
fix: `store ${signature} prices as nonnegative integers and name a volume meter for bps`,
|
|
147
|
+
message: `cost table ${table.version} has an invalid ${signature} price`,
|
|
148
|
+
severity: "error",
|
|
149
|
+
span: action.origin,
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const eventTotal = BigInt(row.perEventMinor) * BigInt(count);
|
|
154
|
+
if (row.payer === "product") productPerEvent += eventTotal;
|
|
155
|
+
else endCustomerPerEvent += eventTotal;
|
|
156
|
+
components.push({
|
|
157
|
+
bps: row.bps,
|
|
158
|
+
count,
|
|
159
|
+
meter: row.meter,
|
|
160
|
+
payer: row.payer,
|
|
161
|
+
perEventMinor: row.perEventMinor,
|
|
162
|
+
settlement: row.settlement,
|
|
163
|
+
signature,
|
|
164
|
+
...(row.volumeMeter ? { volumeMeter: row.volumeMeter } : {}),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
actions.push({
|
|
169
|
+
action: action.name,
|
|
170
|
+
components,
|
|
171
|
+
endCustomerPerEventMinor: endCustomerPerEvent.toString(),
|
|
172
|
+
instrument: instrument.id,
|
|
173
|
+
perEventMinor: productPerEvent.toString(),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const fixed = fixedCost(
|
|
178
|
+
typedStructuralCounts(program, composesCatalogBlueprint),
|
|
179
|
+
table,
|
|
180
|
+
program.origin,
|
|
181
|
+
diagnostics,
|
|
182
|
+
);
|
|
183
|
+
if (diagnostics.length > 0) return { diagnostics, ok: false };
|
|
184
|
+
return { manifest: manifest(table, fixed, actions), ok: true };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function computeUdlFixedCost(
|
|
188
|
+
document: UdlDocument,
|
|
189
|
+
table: UdlCostTable,
|
|
190
|
+
composesCatalogBlueprint: boolean,
|
|
191
|
+
): UdlCostManifest["fixed"] {
|
|
192
|
+
const diagnostics: GeneralDiagnostic[] = [];
|
|
193
|
+
const fixed = fixedCost(
|
|
194
|
+
udlStructuralCounts(document, composesCatalogBlueprint),
|
|
195
|
+
table,
|
|
196
|
+
{ start: 0, end: 0 },
|
|
197
|
+
diagnostics,
|
|
198
|
+
);
|
|
199
|
+
if (diagnostics.length > 0) throw new Error(diagnostics[0]?.message);
|
|
200
|
+
return fixed;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function buildUdlCostManifest(
|
|
204
|
+
document: UdlDocument,
|
|
205
|
+
table: UdlCostTable,
|
|
206
|
+
composesCatalogBlueprint: boolean,
|
|
207
|
+
): UdlCostManifest {
|
|
208
|
+
if (!/^[A-Z]{3}$/.test(table.currency)) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`cost table ${table.version} has invalid billing currency ${table.currency}`,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
const rowsBySignature = new Map<string, UdlCostTable["rows"]>();
|
|
214
|
+
for (const row of table.rows) {
|
|
215
|
+
rowsBySignature.set(row.signature, [
|
|
216
|
+
...(rowsBySignature.get(row.signature) ?? []),
|
|
217
|
+
row,
|
|
218
|
+
]);
|
|
219
|
+
}
|
|
220
|
+
const actions: UdlCostManifest["actions"][number][] = [];
|
|
221
|
+
for (const instrument of document.instruments) {
|
|
222
|
+
for (const [actionName, action] of Object.entries(instrument.actions)) {
|
|
223
|
+
const components: UdlCostManifest["actions"][number]["components"][number][] =
|
|
224
|
+
[];
|
|
225
|
+
let productPerEvent = 0n;
|
|
226
|
+
let endCustomerPerEvent = 0n;
|
|
227
|
+
const effects = (action.effects ?? {}) as Readonly<
|
|
228
|
+
Partial<
|
|
229
|
+
Record<
|
|
230
|
+
HsxEffectKind,
|
|
231
|
+
readonly { readonly signature: string; readonly source: string }[]
|
|
232
|
+
>
|
|
233
|
+
>
|
|
234
|
+
>;
|
|
235
|
+
for (const [signature, count] of effectCounts(effects)) {
|
|
236
|
+
const rows = rowsBySignature.get(signature) ?? [];
|
|
237
|
+
if (rows.length === 0)
|
|
238
|
+
throw new Error(
|
|
239
|
+
`${instrument.id}.${actionName} has unpriced effect signature ${signature}`,
|
|
240
|
+
);
|
|
241
|
+
for (const row of rows) {
|
|
242
|
+
if (!validPrice(row))
|
|
243
|
+
throw new Error(
|
|
244
|
+
`cost table ${table.version} has an invalid ${signature} price`,
|
|
245
|
+
);
|
|
246
|
+
const eventTotal = BigInt(row.perEventMinor) * BigInt(count);
|
|
247
|
+
if (row.payer === "product") productPerEvent += eventTotal;
|
|
248
|
+
else endCustomerPerEvent += eventTotal;
|
|
249
|
+
components.push({
|
|
250
|
+
bps: row.bps,
|
|
251
|
+
count,
|
|
252
|
+
meter: row.meter,
|
|
253
|
+
payer: row.payer,
|
|
254
|
+
perEventMinor: row.perEventMinor,
|
|
255
|
+
settlement: row.settlement,
|
|
256
|
+
signature,
|
|
257
|
+
...(row.volumeMeter ? { volumeMeter: row.volumeMeter } : {}),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
actions.push({
|
|
262
|
+
action: actionName,
|
|
263
|
+
components,
|
|
264
|
+
endCustomerPerEventMinor: endCustomerPerEvent.toString(),
|
|
265
|
+
instrument: instrument.id,
|
|
266
|
+
perEventMinor: productPerEvent.toString(),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return manifest(
|
|
271
|
+
table,
|
|
272
|
+
computeUdlFixedCost(document, table, composesCatalogBlueprint),
|
|
273
|
+
actions,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function evaluateUdlCostManifest(
|
|
278
|
+
costManifest: UdlCostManifest,
|
|
279
|
+
readings: Readonly<Record<string, string>>,
|
|
280
|
+
): string {
|
|
281
|
+
let total = BigInt(costManifest.fixed.monthlyMinor);
|
|
282
|
+
const priced = new Set<string>();
|
|
283
|
+
for (const price of costManifest.meterPrices) {
|
|
284
|
+
if (price.payer !== "product") continue;
|
|
285
|
+
const key = pricingKey(price);
|
|
286
|
+
if (priced.has(key)) continue;
|
|
287
|
+
priced.add(key);
|
|
288
|
+
const count = scaledDecimal(readings[price.meter] ?? "0");
|
|
289
|
+
total += (BigInt(price.perEventMinor) * count) / 1_000_000n;
|
|
290
|
+
if (price.bps > 0 && price.volumeMeter) {
|
|
291
|
+
const volume = scaledDecimal(readings[price.volumeMeter] ?? "0");
|
|
292
|
+
total += (volume * BigInt(price.bps)) / 10_000n / 1_000_000n;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return total.toString();
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function manifest(
|
|
299
|
+
table: UdlCostTable,
|
|
300
|
+
fixed: UdlCostManifest["fixed"],
|
|
301
|
+
actions: UdlCostManifest["actions"],
|
|
302
|
+
): UdlCostManifest {
|
|
303
|
+
const declaredMeters = [...new Set(table.declaredMeters)].sort();
|
|
304
|
+
const declaredMeterSet = new Set(declaredMeters);
|
|
305
|
+
const meterPrices = table.rows.filter(
|
|
306
|
+
(row) =>
|
|
307
|
+
(row.perEventMinor !== "0" || row.bps > 0) &&
|
|
308
|
+
(declaredMeterSet.has(row.meter) ||
|
|
309
|
+
(row.volumeMeter !== undefined &&
|
|
310
|
+
declaredMeterSet.has(row.volumeMeter))),
|
|
311
|
+
);
|
|
312
|
+
const variables = new Set<string>();
|
|
313
|
+
const terms = [fixed.monthlyMinor];
|
|
314
|
+
const priced = new Set<string>();
|
|
315
|
+
for (const price of meterPrices) {
|
|
316
|
+
if (price.payer !== "product") continue;
|
|
317
|
+
const key = pricingKey(price);
|
|
318
|
+
if (priced.has(key)) continue;
|
|
319
|
+
priced.add(key);
|
|
320
|
+
variables.add(price.meter);
|
|
321
|
+
if (price.perEventMinor !== "0")
|
|
322
|
+
terms.push(`${price.perEventMinor} * ${price.meter}`);
|
|
323
|
+
if (price.bps > 0 && price.volumeMeter) {
|
|
324
|
+
variables.add(price.volumeMeter);
|
|
325
|
+
terms.push(`${price.bps} / 10000 * ${price.volumeMeter}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
actions,
|
|
330
|
+
costTableVersion: table.version,
|
|
331
|
+
currency: table.currency,
|
|
332
|
+
declaredMeters,
|
|
333
|
+
effectiveTableDigest: table.effectiveTableDigest,
|
|
334
|
+
fixed,
|
|
335
|
+
meterPrices,
|
|
336
|
+
monthlyEstimate: {
|
|
337
|
+
expression: terms.join(" + "),
|
|
338
|
+
variables: [...variables].sort(),
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function pricingKey(
|
|
344
|
+
component: Pick<
|
|
345
|
+
UdlCostTable["rows"][number],
|
|
346
|
+
| "bps"
|
|
347
|
+
| "meter"
|
|
348
|
+
| "payer"
|
|
349
|
+
| "perEventMinor"
|
|
350
|
+
| "settlement"
|
|
351
|
+
| "signature"
|
|
352
|
+
| "volumeMeter"
|
|
353
|
+
>,
|
|
354
|
+
): string {
|
|
355
|
+
return [
|
|
356
|
+
component.signature,
|
|
357
|
+
component.meter,
|
|
358
|
+
component.volumeMeter ?? "",
|
|
359
|
+
component.perEventMinor,
|
|
360
|
+
String(component.bps),
|
|
361
|
+
component.payer,
|
|
362
|
+
component.settlement,
|
|
363
|
+
].join("\u0000");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function effectCounts(
|
|
367
|
+
effects: Readonly<
|
|
368
|
+
Partial<
|
|
369
|
+
Record<
|
|
370
|
+
HsxEffectKind,
|
|
371
|
+
readonly { readonly signature: string; readonly source: string }[]
|
|
372
|
+
>
|
|
373
|
+
>
|
|
374
|
+
>,
|
|
375
|
+
): readonly [string, number][] {
|
|
376
|
+
const counts = new Map<string, number>();
|
|
377
|
+
for (const kind of udlEffectKinds) {
|
|
378
|
+
for (const effect of effects[kind] ?? [])
|
|
379
|
+
counts.set(effect.signature, (counts.get(effect.signature) ?? 0) + 1);
|
|
380
|
+
}
|
|
381
|
+
return [...counts.entries()].sort(([left], [right]) =>
|
|
382
|
+
left.localeCompare(right),
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function fixedCost(
|
|
387
|
+
counts: Readonly<Record<string, number>>,
|
|
388
|
+
table: UdlCostTable,
|
|
389
|
+
span: { readonly start: number; readonly end: number },
|
|
390
|
+
diagnostics: GeneralDiagnostic[],
|
|
391
|
+
): UdlCostManifest["fixed"] {
|
|
392
|
+
const components: UdlCostManifest["fixed"]["components"][number][] = [];
|
|
393
|
+
let complexityScore = 0;
|
|
394
|
+
for (const [weight, count] of Object.entries(counts)) {
|
|
395
|
+
if (count === 0) continue;
|
|
396
|
+
const value = table.fixed.weights[weight];
|
|
397
|
+
if (value === undefined || !Number.isSafeInteger(value) || value < 0) {
|
|
398
|
+
diagnostics.push({
|
|
399
|
+
code: "HSX1302",
|
|
400
|
+
fix: `add a nonnegative integer ${weight} weight to cost table ${table.version}`,
|
|
401
|
+
message: `cost table ${table.version} has an invalid ${weight} complexity weight`,
|
|
402
|
+
severity: "error",
|
|
403
|
+
span,
|
|
404
|
+
});
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
complexityScore += value * count;
|
|
408
|
+
components.push({ count, weight });
|
|
409
|
+
}
|
|
410
|
+
for (const [field, value] of Object.entries(table.fixed).filter(
|
|
411
|
+
([key]) => key !== "weights",
|
|
412
|
+
)) {
|
|
413
|
+
if (typeof value === "string" && nonnegativeInteger(value)) continue;
|
|
414
|
+
diagnostics.push({
|
|
415
|
+
code: "HSX1302",
|
|
416
|
+
fix: `store fixed.${field} as a nonnegative decimal integer`,
|
|
417
|
+
message: `cost table ${table.version} has an invalid fixed ${field} price`,
|
|
418
|
+
severity: "error",
|
|
419
|
+
span,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
activationMinor: (
|
|
424
|
+
integerOrZero(table.fixed.activationBaseMinor) +
|
|
425
|
+
integerOrZero(table.fixed.activationPerPointMinor) *
|
|
426
|
+
BigInt(complexityScore)
|
|
427
|
+
).toString(),
|
|
428
|
+
complexityScore,
|
|
429
|
+
components,
|
|
430
|
+
monthlyMinor: (
|
|
431
|
+
integerOrZero(table.fixed.monthlyBaseMinor) +
|
|
432
|
+
integerOrZero(table.fixed.monthlyPerPointMinor) * BigInt(complexityScore)
|
|
433
|
+
).toString(),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function typedStructuralCounts(
|
|
438
|
+
program: TypedProgram,
|
|
439
|
+
blueprint: boolean,
|
|
440
|
+
): Readonly<Record<string, number>> {
|
|
441
|
+
const subjectKinds = new Set<string>();
|
|
442
|
+
const decisions = new Set<string>();
|
|
443
|
+
let flow = 0,
|
|
444
|
+
move = 0,
|
|
445
|
+
unwind = 0,
|
|
446
|
+
aggregate = 0,
|
|
447
|
+
gate = 0,
|
|
448
|
+
timer = 0;
|
|
449
|
+
for (const instrument of program.instruments) {
|
|
450
|
+
for (const kind of arrayValue(objectValue(instrument.slots.subject)?.kinds))
|
|
451
|
+
if (typeof kind === "string") subjectKinds.add(kind);
|
|
452
|
+
if (instrument.actions.some((action) => action.slots.quote !== undefined))
|
|
453
|
+
unwind += 1;
|
|
454
|
+
aggregate += arrayValue(instrument.slots.aggregateInvariants).length;
|
|
455
|
+
for (const action of instrument.actions) {
|
|
456
|
+
flow += 1;
|
|
457
|
+
move += arrayValue(action.slots.moves).length;
|
|
458
|
+
gate += arrayValue(action.slots.requiresRefs).length;
|
|
459
|
+
if (action.slots.deadline !== undefined) timer += 1;
|
|
460
|
+
if (action.slots.due !== undefined) timer += 1;
|
|
461
|
+
const capability = objectValue(action.slots.decision)?.capability;
|
|
462
|
+
if (typeof capability === "string") decisions.add(capability);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return countRecord(
|
|
466
|
+
program.instruments.length,
|
|
467
|
+
subjectKinds.size,
|
|
468
|
+
flow,
|
|
469
|
+
move,
|
|
470
|
+
decisions.size,
|
|
471
|
+
unwind,
|
|
472
|
+
aggregate,
|
|
473
|
+
gate,
|
|
474
|
+
timer,
|
|
475
|
+
blueprint,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function udlStructuralCounts(
|
|
480
|
+
document: UdlDocument,
|
|
481
|
+
blueprint: boolean,
|
|
482
|
+
): Readonly<Record<string, number>> {
|
|
483
|
+
const subjectKinds = new Set<string>();
|
|
484
|
+
const decisions = new Set<string>();
|
|
485
|
+
let flow = 0,
|
|
486
|
+
move = 0,
|
|
487
|
+
unwind = 0,
|
|
488
|
+
aggregate = 0,
|
|
489
|
+
gate = 0,
|
|
490
|
+
timer = 0;
|
|
491
|
+
for (const instrument of document.instruments) {
|
|
492
|
+
for (const kind of instrument.subject?.kinds ?? []) subjectKinds.add(kind);
|
|
493
|
+
if (Object.values(instrument.actions).some((action) => action.quote))
|
|
494
|
+
unwind += 1;
|
|
495
|
+
aggregate += instrument.aggregateInvariants?.length ?? 0;
|
|
496
|
+
for (const action of Object.values(instrument.actions)) {
|
|
497
|
+
flow += 1;
|
|
498
|
+
move += action.moves.length;
|
|
499
|
+
gate += action.requiresRefs?.length ?? 0;
|
|
500
|
+
if (action.deadline) timer += 1;
|
|
501
|
+
if (action.due) timer += 1;
|
|
502
|
+
if (action.decision) decisions.add(action.decision.capability);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
return countRecord(
|
|
506
|
+
document.instruments.length,
|
|
507
|
+
subjectKinds.size,
|
|
508
|
+
flow,
|
|
509
|
+
move,
|
|
510
|
+
decisions.size,
|
|
511
|
+
unwind,
|
|
512
|
+
aggregate,
|
|
513
|
+
gate,
|
|
514
|
+
timer,
|
|
515
|
+
blueprint,
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function countRecord(
|
|
520
|
+
instrument: number,
|
|
521
|
+
subjectKind: number,
|
|
522
|
+
flow: number,
|
|
523
|
+
move: number,
|
|
524
|
+
providerDecision: number,
|
|
525
|
+
unwind: number,
|
|
526
|
+
aggregateInvariant: number,
|
|
527
|
+
gate: number,
|
|
528
|
+
timer: number,
|
|
529
|
+
blueprint: boolean,
|
|
530
|
+
): Readonly<Record<string, number>> {
|
|
531
|
+
return {
|
|
532
|
+
instrument,
|
|
533
|
+
subject_kind: subjectKind,
|
|
534
|
+
flow,
|
|
535
|
+
move,
|
|
536
|
+
provider_decision: providerDecision,
|
|
537
|
+
unwind,
|
|
538
|
+
aggregate_invariant: aggregateInvariant,
|
|
539
|
+
gate,
|
|
540
|
+
timer,
|
|
541
|
+
blueprint: blueprint ? 1 : 0,
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function validPrice(row: UdlCostTable["rows"][number]): boolean {
|
|
546
|
+
return (
|
|
547
|
+
nonnegativeInteger(row.perEventMinor) &&
|
|
548
|
+
Number.isSafeInteger(row.bps) &&
|
|
549
|
+
row.bps >= 0 &&
|
|
550
|
+
(row.settlement === "per_event" || row.settlement === "invoice") &&
|
|
551
|
+
(row.bps === 0 || row.volumeMeter !== undefined)
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function arrayValue(value: JsonValue | undefined): readonly JsonValue[] {
|
|
556
|
+
return Array.isArray(value) ? value : [];
|
|
557
|
+
}
|
|
558
|
+
function objectValue(
|
|
559
|
+
value: JsonValue | undefined,
|
|
560
|
+
): Readonly<Record<string, JsonValue>> | undefined {
|
|
561
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
562
|
+
? (value as Readonly<Record<string, JsonValue>>)
|
|
563
|
+
: undefined;
|
|
564
|
+
}
|
|
565
|
+
function nonnegativeInteger(value: string): boolean {
|
|
566
|
+
return /^(0|[1-9][0-9]*)$/.test(value);
|
|
567
|
+
}
|
|
568
|
+
function integerOrZero(value: string): bigint {
|
|
569
|
+
return nonnegativeInteger(value) ? BigInt(value) : 0n;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function scaledDecimal(value: string): bigint {
|
|
573
|
+
const match = /^(0|[1-9][0-9]*)(?:\.([0-9]{1,6}))?$/.exec(value);
|
|
574
|
+
if (!match) throw new Error(`invalid meter reading ${value}`);
|
|
575
|
+
return (
|
|
576
|
+
BigInt(match[1] ?? "0") * 1_000_000n +
|
|
577
|
+
BigInt((match[2] ?? "").padEnd(6, "0"))
|
|
578
|
+
);
|
|
579
|
+
}
|