@hyperscale0/hsx 1.0.0 → 2.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.
Files changed (166) hide show
  1. package/CHANGELOG.md +15 -4
  2. package/README.md +58 -38
  3. package/dist/src/ast.d.ts +4 -2
  4. package/dist/src/ast.d.ts.map +1 -1
  5. package/dist/src/cli.d.ts +1 -1
  6. package/dist/src/cli.js.map +1 -1
  7. package/dist/src/compile.d.ts +8 -2
  8. package/dist/src/compile.d.ts.map +1 -1
  9. package/dist/src/compile.js +8 -1
  10. package/dist/src/compile.js.map +1 -1
  11. package/dist/src/cost.d.ts +3 -1
  12. package/dist/src/cost.d.ts.map +1 -1
  13. package/dist/src/cost.js +124 -94
  14. package/dist/src/cost.js.map +1 -1
  15. package/dist/src/diagnostics.d.ts.map +1 -1
  16. package/dist/src/diagnostics.js +89 -5
  17. package/dist/src/diagnostics.js.map +1 -1
  18. package/dist/src/emit.d.ts +3 -1
  19. package/dist/src/emit.d.ts.map +1 -1
  20. package/dist/src/emit.js +27 -10
  21. package/dist/src/emit.js.map +1 -1
  22. package/dist/src/format.js.map +1 -1
  23. package/dist/src/index.d.ts +2 -2
  24. package/dist/src/index.d.ts.map +1 -1
  25. package/dist/src/index.js +1 -1
  26. package/dist/src/index.js.map +1 -1
  27. package/dist/src/ir.d.ts.map +1 -1
  28. package/dist/src/lex.d.ts.map +1 -1
  29. package/dist/src/lsp/server.js.map +1 -1
  30. package/dist/src/modules.js +7 -3
  31. package/dist/src/modules.js.map +1 -1
  32. package/dist/src/parse.js +3 -3
  33. package/dist/src/parse.js.map +1 -1
  34. package/dist/src/std-bundle.js +41 -41
  35. package/dist/src/std-bundle.js.map +1 -1
  36. package/dist/src/std-library.js +2 -2
  37. package/dist/src/typecheck.d.ts.map +1 -1
  38. package/dist/src/typecheck.js +859 -156
  39. package/dist/src/typecheck.js.map +1 -1
  40. package/dist/src/version.d.ts +1 -1
  41. package/dist/src/version.js +1 -1
  42. package/docs/README.md +10 -1
  43. package/docs/assets/hsx.svg +18 -0
  44. package/docs/guide/01-first-program.md +1 -1
  45. package/docs/guide/02-money.md +1 -1
  46. package/docs/guide/03-instruments.md +44 -3
  47. package/docs/guide/04-lifecycles.md +18 -3
  48. package/docs/guide/05-fees-and-splits.md +2 -2
  49. package/docs/guide/06-schedules.md +1 -1
  50. package/docs/guide/07-composition.md +8 -2
  51. package/docs/guide/08-writing-a-module.md +13 -2
  52. package/docs/guide/09-cost.md +3 -3
  53. package/docs/guide/10-diagnostics.md +1 -1
  54. package/docs/llms-full.txt +453 -190
  55. package/docs/llms.txt +2 -2
  56. package/docs/playground.md +4 -3
  57. package/docs/reference/cli.md +2 -2
  58. package/docs/reference/diagnostics.md +223 -37
  59. package/docs/reference/grammar.md +2 -2
  60. package/docs/reference/std/advance.md +2 -2
  61. package/docs/reference/std/cancellable_booking.md +10 -10
  62. package/docs/reference/std/captured_payment.md +14 -14
  63. package/docs/reference/std/conditional_disbursement.md +3 -3
  64. package/docs/reference/std/credit_facility.md +2 -2
  65. package/docs/reference/std/held_payment.md +37 -33
  66. package/docs/reference/std/instant_transfer.md +8 -8
  67. package/docs/reference/std/metered.md +2 -2
  68. package/docs/reference/std/pooled_split.md +2 -2
  69. package/docs/reference/std/premium_forward.md +13 -13
  70. package/docs/reference/std/reconciled_payout.md +2 -2
  71. package/docs/reference/std/recurring_collection.md +2 -2
  72. package/docs/reference/std/rotating_pool.md +2 -2
  73. package/docs/reference/std/scheduled.md +18 -18
  74. package/docs/reference/std/security_deposit.md +14 -14
  75. package/docs/reference/std/settlement_batch.md +2 -2
  76. package/docs/reference/std/swap.md +18 -18
  77. package/docs/reference/std/threshold_pool.md +2 -2
  78. package/docs/reference/std/weighted_distribution.md +2 -2
  79. package/docs/reference/types.md +1 -1
  80. package/docs/reference/udl-output.md +1 -1
  81. package/examples/01-first-program/README.md +1 -1
  82. package/examples/01-first-program/tip-jar.hsx +1 -1
  83. package/examples/02-imports-and-archetypes/photo-booth.hsx +1 -1
  84. package/examples/03-diagnostics/corner-shop-fixed.hsx +1 -1
  85. package/examples/03-diagnostics/corner-shop.hsx +1 -1
  86. package/examples/04-complete-product/study-hall.hsx +1 -1
  87. package/examples/05-watch-club/README.md +0 -3
  88. package/examples/05-watch-club/watch-club.hsx +1 -1
  89. package/examples/README.md +35 -4
  90. package/examples/advance/advance.hsx +1 -1
  91. package/examples/advance/advance.udl +10 -5
  92. package/examples/cancellable_booking/cancellable_booking.hsx +1 -1
  93. package/examples/cancellable_booking/cancellable_booking.udl +3 -1
  94. package/examples/captured_payment/captured_payment.hsx +1 -1
  95. package/examples/captured_payment/captured_payment.udl +4 -1
  96. package/examples/conditional_disbursement/conditional_disbursement.hsx +1 -1
  97. package/examples/conditional_disbursement/conditional_disbursement.udl +5 -3
  98. package/examples/cost-table.json +1602 -382
  99. package/examples/credit_facility/credit_facility.hsx +1 -1
  100. package/examples/credit_facility/credit_facility.udl +16 -9
  101. package/examples/held_payment/held_payment.hsx +1 -1
  102. package/examples/held_payment/held_payment.udl +38 -3
  103. package/examples/instant_transfer/instant_transfer.hsx +1 -1
  104. package/examples/instant_transfer/instant_transfer.udl +11 -5
  105. package/examples/metered/metered.hsx +1 -1
  106. package/examples/metered/metered.udl +4 -2
  107. package/examples/pooled_split/pooled_split.hsx +1 -1
  108. package/examples/pooled_split/pooled_split.udl +6 -3
  109. package/examples/premium_forward/premium_forward.hsx +1 -1
  110. package/examples/premium_forward/premium_forward.udl +10 -4
  111. package/examples/reconciled_payout/reconciled_payout.hsx +1 -1
  112. package/examples/reconciled_payout/reconciled_payout.udl +4 -2
  113. package/examples/recurring_collection/recurring_collection.hsx +1 -1
  114. package/examples/recurring_collection/recurring_collection.udl +12 -7
  115. package/examples/rotating_pool/rotating_pool.hsx +1 -1
  116. package/examples/rotating_pool/rotating_pool.udl +14 -4
  117. package/examples/scheduled/scheduled.hsx +1 -1
  118. package/examples/scheduled/scheduled.udl +9 -4
  119. package/examples/security_deposit/security_deposit.hsx +1 -1
  120. package/examples/security_deposit/security_deposit.udl +17 -3
  121. package/examples/settlement_batch/settlement_batch.hsx +1 -1
  122. package/examples/swap/swap.hsx +1 -1
  123. package/examples/swap/swap.udl +12 -6
  124. package/examples/threshold_pool/threshold_pool.hsx +1 -1
  125. package/examples/threshold_pool/threshold_pool.udl +4 -2
  126. package/examples/weighted_distribution/weighted_distribution.hsx +1 -1
  127. package/examples/weighted_distribution/weighted_distribution.udl +4 -2
  128. package/package.json +6 -5
  129. package/skills/hsx/SKILL.md +26 -26
  130. package/src/ast.ts +4 -2
  131. package/src/cli.ts +4 -4
  132. package/src/compile.ts +14 -3
  133. package/src/cost.ts +176 -104
  134. package/src/diagnostics.ts +89 -4
  135. package/src/emit.ts +32 -18
  136. package/src/index.ts +2 -1
  137. package/src/modules.ts +9 -3
  138. package/src/parse.ts +3 -3
  139. package/src/std-bundle.ts +41 -41
  140. package/src/std-library.ts +2 -2
  141. package/src/typecheck.ts +1147 -235
  142. package/src/version.ts +1 -1
  143. package/std/SEMANTICS.md +12 -3
  144. package/std/{settlements → money_flows}/advance.hsx +1 -1
  145. package/std/{settlements → money_flows}/cancellable_booking.hsx +4 -3
  146. package/std/{settlements → money_flows}/captured_payment.hsx +35 -6
  147. package/std/{settlements → money_flows}/conditional_disbursement.hsx +17 -16
  148. package/std/{settlements → money_flows}/credit_facility.hsx +1 -2
  149. package/std/{settlements → money_flows}/held_payment.hsx +59 -25
  150. package/std/money_flows/index.hsx +3 -0
  151. package/std/{settlements → money_flows}/instant_transfer.hsx +2 -3
  152. package/std/{settlements → money_flows}/metered.hsx +1 -1
  153. package/std/{settlements → money_flows}/pooled_split.hsx +3 -3
  154. package/std/{settlements → money_flows}/premium_forward.hsx +22 -23
  155. package/std/{settlements → money_flows}/reconciled_payout.hsx +3 -3
  156. package/std/{settlements → money_flows}/recurring_collection.hsx +1 -1
  157. package/std/{settlements → money_flows}/rotating_pool.hsx +8 -6
  158. package/std/{settlements → money_flows}/scheduled.hsx +22 -27
  159. package/std/{settlements → money_flows}/security_deposit.hsx +38 -14
  160. package/std/{settlements → money_flows}/settlement_batch.hsx +3 -3
  161. package/std/{settlements → money_flows}/swap.hsx +41 -16
  162. package/std/{settlements → money_flows}/threshold_pool.hsx +5 -5
  163. package/std/{settlements → money_flows}/weighted_distribution.hsx +6 -6
  164. package/docs/sessions/2026-09-02-two-instruments.hsx +0 -21
  165. package/docs/sessions/2026-09-02-two-instruments.md +0 -41
  166. package/std/settlements/index.hsx +0 -3
package/src/cost.ts CHANGED
@@ -3,10 +3,17 @@ import {
3
3
  type UdlDocument,
4
4
  type UdlEffectKind,
5
5
  } from "@hyperscale0/udl";
6
- import type { GeneralDiagnostic, JsonValue, TypedProgram } from "./ir.ts";
6
+ import type {
7
+ GeneralDiagnostic,
8
+ JsonValue,
9
+ TypedField,
10
+ TypedProgram,
11
+ } from "./ir.ts";
7
12
 
8
13
  export type HsxEffectKind = UdlEffectKind;
9
14
 
15
+ const CURRENCY = /^[A-Z]{3}$/;
16
+
10
17
  export interface UdlCostTable {
11
18
  readonly version: string;
12
19
  /** Digest of the effective prices and fixed anchors after overrides. */
@@ -72,16 +79,20 @@ export interface UdlCostManifest {
72
79
  };
73
80
  }
74
81
 
82
+ /** One cost table, or one per billing currency the host prices. */
83
+ export type UdlCostTables = UdlCostTable | readonly UdlCostTable[];
84
+
75
85
  export type CostManifestResult =
76
86
  | { readonly diagnostics: readonly GeneralDiagnostic[]; readonly ok: false }
77
87
  | { readonly manifest: UdlCostManifest; readonly ok: true };
78
88
 
79
89
  export function buildCostManifest(
80
90
  program: TypedProgram,
81
- table: UdlCostTable | undefined,
91
+ tables: UdlCostTables | undefined,
82
92
  composesCatalogBlueprint: boolean,
83
93
  ): CostManifestResult {
84
- if (!table) {
94
+ const priced = tables === undefined ? [] : [tables].flat();
95
+ if (priced.length === 0) {
85
96
  return {
86
97
  diagnostics: [
87
98
  {
@@ -97,13 +108,16 @@ export function buildCostManifest(
97
108
  };
98
109
  }
99
110
 
100
- if (!/^[A-Z]{3}$/.test(table.currency)) {
111
+ const malformed = priced.find(
112
+ (candidate) => !CURRENCY.test(candidate.currency),
113
+ );
114
+ if (malformed) {
101
115
  return {
102
116
  diagnostics: [
103
117
  {
104
118
  code: "HSX1303",
105
119
  fix: "name one three-letter uppercase billing currency on the cost table",
106
- message: `cost table ${table.version} has invalid billing currency ${table.currency}`,
120
+ message: `cost table ${malformed.version} has invalid billing currency ${malformed.currency}`,
107
121
  severity: "error",
108
122
  span: program.origin,
109
123
  },
@@ -113,65 +127,39 @@ export function buildCostManifest(
113
127
  }
114
128
 
115
129
  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
- }
130
+ const table = ledgerCostTable(program, priced, diagnostics);
131
+ if (!table) return { diagnostics, ok: false };
132
+ const rowsBySignature = indexCostRows(table);
123
133
  const actions: UdlCostManifest["actions"][number][] = [];
124
134
  for (const instrument of program.instruments) {
125
135
  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
- });
136
+ actions.push(
137
+ priceAction(
138
+ instrument.id,
139
+ action.name,
140
+ effectCounts(action.effects),
141
+ rowsBySignature,
142
+ (issue) => {
143
+ if (issue.kind === "missing_price") {
144
+ diagnostics.push({
145
+ code: "HSX1301",
146
+ fix: `add ${issue.signature} to cost table ${table.version}`,
147
+ message: `${instrument.id}.${action.name} has unpriced effect signature ${issue.signature}`,
148
+ severity: "error",
149
+ span: action.origin,
150
+ });
151
+ } else {
152
+ diagnostics.push({
153
+ code: "HSX1302",
154
+ fix: `store ${issue.signature} prices as nonnegative integers and name a volume meter for bps`,
155
+ message: `cost table ${table.version} has an invalid ${issue.signature} price`,
156
+ severity: "error",
157
+ span: action.origin,
158
+ });
159
+ }
160
+ },
161
+ ),
162
+ );
175
163
  }
176
164
  }
177
165
  const fixed = fixedCost(
@@ -184,6 +172,52 @@ export function buildCostManifest(
184
172
  return { manifest: manifest(table, fixed, actions), ok: true };
185
173
  }
186
174
 
175
+ /**
176
+ * The table priced in the program's ledger currency: the one currency its
177
+ * money fields move. A program without money fields prices in the host's
178
+ * first table.
179
+ */
180
+ function ledgerCostTable(
181
+ program: TypedProgram,
182
+ tables: readonly UdlCostTable[],
183
+ diagnostics: GeneralDiagnostic[],
184
+ ): UdlCostTable | undefined {
185
+ const currencies = new Map<string, TypedField["origin"]>();
186
+ for (const instrument of program.instruments) {
187
+ for (const field of instrument.fields) {
188
+ // An unbound currency parameter (money<C>) is decided per instance,
189
+ // not by the program, so it names no ledger currency.
190
+ const currency = field.type.currency;
191
+ if (field.type.kind !== "money" || !currency || !CURRENCY.test(currency))
192
+ continue;
193
+ if (!currencies.has(currency)) currencies.set(currency, field.origin);
194
+ }
195
+ }
196
+ const [first, second] = [...currencies];
197
+ if (second) {
198
+ diagnostics.push({
199
+ code: "HSX1305",
200
+ fix: `move all money in ${first![0]}; a program bills in one ledger currency`,
201
+ message: `program moves money in ${[...currencies.keys()].join(" and ")}; a program bills in one ledger currency`,
202
+ severity: "error",
203
+ span: second[1],
204
+ });
205
+ return undefined;
206
+ }
207
+ const ledger = first?.[0] ?? tables[0]!.currency;
208
+ const table = tables.find((candidate) => candidate.currency === ledger);
209
+ if (table) return table;
210
+ const priced = tables.map((candidate) => candidate.currency).join(", ");
211
+ diagnostics.push({
212
+ code: "HSX1304",
213
+ fix: `move money in a priced currency: ${priced}`,
214
+ message: `program moves money in ${ledger}, which no cost table prices (priced: ${priced})`,
215
+ severity: "error",
216
+ span: first?.[1] ?? program.origin,
217
+ });
218
+ return undefined;
219
+ }
220
+
187
221
  export function computeUdlFixedCost(
188
222
  document: UdlDocument,
189
223
  table: UdlCostTable,
@@ -205,25 +239,15 @@ export function buildUdlCostManifest(
205
239
  table: UdlCostTable,
206
240
  composesCatalogBlueprint: boolean,
207
241
  ): UdlCostManifest {
208
- if (!/^[A-Z]{3}$/.test(table.currency)) {
242
+ if (!CURRENCY.test(table.currency)) {
209
243
  throw new Error(
210
244
  `cost table ${table.version} has invalid billing currency ${table.currency}`,
211
245
  );
212
246
  }
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
- }
247
+ const rowsBySignature = indexCostRows(table);
220
248
  const actions: UdlCostManifest["actions"][number][] = [];
221
249
  for (const instrument of document.instruments) {
222
250
  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
251
  const effects = (action.effects ?? {}) as Readonly<
228
252
  Partial<
229
253
  Record<
@@ -232,39 +256,24 @@ export function buildUdlCostManifest(
232
256
  >
233
257
  >
234
258
  >;
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))
259
+ actions.push(
260
+ priceAction(
261
+ instrument.id,
262
+ actionName,
263
+ effectCounts(effects),
264
+ rowsBySignature,
265
+ (issue) => {
266
+ if (issue.kind === "missing_price") {
267
+ throw new Error(
268
+ `${instrument.id}.${actionName} has unpriced effect signature ${issue.signature}`,
269
+ );
270
+ }
243
271
  throw new Error(
244
- `cost table ${table.version} has an invalid ${signature} price`,
272
+ `cost table ${table.version} has an invalid ${issue.signature} price`,
245
273
  );
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
- });
274
+ },
275
+ ),
276
+ );
268
277
  }
269
278
  }
270
279
  return manifest(
@@ -295,6 +304,69 @@ export function evaluateUdlCostManifest(
295
304
  return total.toString();
296
305
  }
297
306
 
307
+ interface ActionPricingIssue {
308
+ readonly kind: "invalid_price" | "missing_price";
309
+ readonly signature: string;
310
+ }
311
+
312
+ function indexCostRows(
313
+ table: UdlCostTable,
314
+ ): ReadonlyMap<string, readonly UdlCostTable["rows"][number][]> {
315
+ const rowsBySignature = new Map<string, UdlCostTable["rows"][number][]>();
316
+ for (const row of table.rows) {
317
+ const existing = rowsBySignature.get(row.signature);
318
+ if (existing) existing.push(row);
319
+ else rowsBySignature.set(row.signature, [row]);
320
+ }
321
+ return rowsBySignature;
322
+ }
323
+
324
+ function priceAction(
325
+ instrument: string,
326
+ action: string,
327
+ counts: Iterable<readonly [string, number]>,
328
+ rowsBySignature: ReadonlyMap<string, readonly UdlCostTable["rows"][number][]>,
329
+ onIssue: (issue: ActionPricingIssue) => void,
330
+ ): UdlCostManifest["actions"][number] {
331
+ const components: UdlCostManifest["actions"][number]["components"][number][] =
332
+ [];
333
+ let productPerEvent = 0n;
334
+ let endCustomerPerEvent = 0n;
335
+ for (const [signature, count] of counts) {
336
+ const rows = rowsBySignature.get(signature);
337
+ if (!rows || rows.length === 0) {
338
+ onIssue({ kind: "missing_price", signature });
339
+ continue;
340
+ }
341
+ for (const row of rows) {
342
+ if (!validPrice(row)) {
343
+ onIssue({ kind: "invalid_price", signature });
344
+ continue;
345
+ }
346
+ const eventTotal = BigInt(row.perEventMinor) * BigInt(count);
347
+ if (row.payer === "product") productPerEvent += eventTotal;
348
+ else endCustomerPerEvent += eventTotal;
349
+ components.push({
350
+ bps: row.bps,
351
+ count,
352
+ meter: row.meter,
353
+ payer: row.payer,
354
+ perEventMinor: row.perEventMinor,
355
+ settlement: row.settlement,
356
+ signature,
357
+ ...(row.volumeMeter ? { volumeMeter: row.volumeMeter } : {}),
358
+ });
359
+ }
360
+ }
361
+ return {
362
+ action,
363
+ components,
364
+ endCustomerPerEventMinor: endCustomerPerEvent.toString(),
365
+ instrument,
366
+ perEventMinor: productPerEvent.toString(),
367
+ };
368
+ }
369
+
298
370
  function manifest(
299
371
  table: UdlCostTable,
300
372
  fixed: UdlCostManifest["fixed"],
@@ -9,11 +9,16 @@ export interface HsxDiagnosticCatalogEntry {
9
9
 
10
10
  const instrument = (
11
11
  body: string,
12
+ fields = "",
12
13
  ): string => `program catalog_probe "Catalog probe"
13
14
  instrument probe {
14
- fields {}
15
+ agent_description: "Probe instrument for catalog diagnostics.";
16
+ fields { ${fields} }
15
17
  lifecycle { states created; initial created; }
16
- action create { steps: []; }
18
+ action create {
19
+ agent_description: "Create a probe instance.";
20
+ steps: [];
21
+ }
17
22
  ${body}
18
23
  }`;
19
24
 
@@ -201,8 +206,8 @@ instrument probe = template(value: "one", value: "two")`,
201
206
  {
202
207
  code: "HSX1022",
203
208
  stage: "typecheck",
204
- title: "Authored instrument in a composition",
205
- fix: "Publish the instrument separately, then select it with use.",
209
+ title: "Authored instrument collides with published catalog",
210
+ fix: "Rename the authored instrument to avoid colliding with published catalog instruments.",
206
211
  // This diagnostic requires a compiler-host published catalog.
207
212
  example: null,
208
213
  reason: "This refusal requires a compiler-host published catalog.",
@@ -214,6 +219,44 @@ instrument probe = template(value: "one", value: "two")`,
214
219
  fix: "List declared, non-optional field names once each.",
215
220
  example: instrument("required: missing;"),
216
221
  },
222
+ {
223
+ code: "HSX1024",
224
+ stage: "typecheck",
225
+ title: "Decision party has no matching account binding",
226
+ fix: "Bind the allowed party to an account field and use that binding in the decision action.",
227
+ example: `program decision_binding "Decision binding"
228
+ party buyer: person
229
+ party seller: business
230
+ port approve { allowed: [buyer] }
231
+ instrument gated(decision: condition) {
232
+ fields {}
233
+ parties { payer: seller }
234
+ lifecycle { states created done; initial created; on approve: created -> done; }
235
+ action create { steps: []; }
236
+ action [decision] { steps: []; port { allowed_parties: [payer]; } }
237
+ }
238
+ instrument gate = gated(decision: port approve)`,
239
+ },
240
+ {
241
+ code: "HSX1025",
242
+ stage: "typecheck",
243
+ title: "Unknown decision port shape type",
244
+ fix: "Declare the shape field as text, money(CUR), date, integer, boolean, account(CUR), ref<instrument_id>, id(instrument_id), bps, or percent.",
245
+ example: `program catalog_probe "Catalog probe"
246
+ import { security_deposit } from "std/money_flows"
247
+ party buyer: person
248
+ party seller: business
249
+ settlement hold = security_deposit {
250
+ payer: buyer
251
+ holder: seller
252
+ amount: depositAmount: money(SAR)
253
+ claim: port report_damage
254
+ claim_amount: { field: damageAmount, bound: depositAmount, remainder: return }
255
+ return: port pass_inspection
256
+ }
257
+ port report_damage { allowed: [seller] shape { damageAmount: money(SAR) } }
258
+ port pass_inspection { allowed: [seller] shape { invalidField: mystery_type } }`,
259
+ },
217
260
  {
218
261
  code: "HSX1101",
219
262
  stage: "typecheck",
@@ -298,6 +341,20 @@ action pay { computes remainder rest { amount_ref: total; on_zero: refuse; total
298
341
  reason:
299
342
  "This refusal requires an invalid compiler-host cost-table currency.",
300
343
  },
344
+ {
345
+ code: "HSX1304",
346
+ stage: "typecheck",
347
+ title: "Unpriced ledger currency",
348
+ fix: "Move money in a currency the shipped cost tables price.",
349
+ example: instrument("", "amount: money<JPY>;"),
350
+ },
351
+ {
352
+ code: "HSX1305",
353
+ stage: "typecheck",
354
+ title: "More than one ledger currency",
355
+ fix: "Move all money in one currency; a program bills in one ledger currency.",
356
+ example: instrument("", "amount: money<SAR>; fee: money<USD>;"),
357
+ },
301
358
  {
302
359
  code: "HSX1401",
303
360
  stage: "typecheck",
@@ -411,6 +468,34 @@ instrument probe = template()`,
411
468
  example: `program catalog_probe "Catalog probe"
412
469
  export instrument template() { fields {}; lifecycle { states created; initial created; }; action create { steps: []; }; }
413
470
  instrument probe = template() { fields: {}; }`,
471
+ },
472
+ {
473
+ code: "HSX1508",
474
+ stage: "typecheck",
475
+ title: "Invalid action-level port syntax",
476
+ fix: "Use allowed_parties: [...] inside an action-level port clause; allowed: is for top-level port declarations.",
477
+ example: `program catalog_probe "Catalog probe"
478
+ party approver: person
479
+ instrument probe {
480
+ fields {}
481
+ lifecycle { states created; initial created; }
482
+ action create {
483
+ steps: [];
484
+ port { allowed: [approver]; }
485
+ }
486
+ }`,
487
+ },
488
+ {
489
+ code: "HSX1509",
490
+ stage: "typecheck",
491
+ title: "Callable action without an agent description",
492
+ fix: 'Add agent_description: "..." to the instrument and to every action a caller can reach; composer.check refuses the program without them.',
493
+ example: `program catalog_probe "Catalog probe"
494
+ instrument probe {
495
+ fields {}
496
+ lifecycle { states created; initial created; }
497
+ action create { steps: []; }
498
+ }`,
414
499
  },
415
500
  {
416
501
  code: "HSX1601",
package/src/emit.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import {
2
- serializeUdl,
3
2
  validateUdl,
4
3
  type UdlDocument,
5
4
  type UdlIssueCode,
@@ -25,7 +24,6 @@ export type GeneralLowerResult =
25
24
  | {
26
25
  readonly ok: true;
27
26
  readonly value: {
28
- readonly canonicalUdl: string;
29
27
  readonly document: UdlDocument;
30
28
  readonly originMap: readonly OriginMapEntry[];
31
29
  };
@@ -72,7 +70,9 @@ export function lowerGeneralProgram(program: TypedProgram): GeneralLowerResult {
72
70
  ok: false,
73
71
  };
74
72
  }
75
- const validation = validateUdl(candidate);
73
+ const validation = validateUdl(candidate, {
74
+ requireDecisionPartyBindings: true,
75
+ });
76
76
  if (!validation.ok) {
77
77
  const origins = originMapFor(program);
78
78
  return {
@@ -94,7 +94,6 @@ export function lowerGeneralProgram(program: TypedProgram): GeneralLowerResult {
94
94
  return {
95
95
  ok: true,
96
96
  value: {
97
- canonicalUdl: serializeUdl(document),
98
97
  document,
99
98
  originMap: originMapFor(program),
100
99
  },
@@ -129,21 +128,36 @@ function unresolvedCompilerMarkers(
129
128
  });
130
129
  }
131
130
 
132
- function originForUdlPath(
131
+ export function originForUdlPath<T extends { readonly path: string }>(
133
132
  path: string,
134
- origins: readonly OriginMapEntry[],
135
- ): OriginMapEntry | undefined {
136
- return (
137
- [...origins]
138
- .filter(
139
- (entry) =>
140
- path === entry.path ||
141
- path.startsWith(`${entry.path}.`) ||
142
- path.startsWith(`${entry.path}[`),
143
- )
144
- .sort((left, right) => right.path.length - left.path.length)[0] ??
145
- origins.find((entry) => entry.path === "$")
146
- );
133
+ origins: readonly T[],
134
+ ): T | undefined {
135
+ let longestMatch: T | undefined;
136
+ let longestLength = -1;
137
+ let rootFallback: T | undefined;
138
+
139
+ for (let i = 0; i < origins.length; i += 1) {
140
+ const entry = origins[i]!;
141
+ const entryPath = entry.path;
142
+ if (rootFallback === undefined && entryPath === "$") {
143
+ rootFallback = entry;
144
+ }
145
+ const entryLen = entryPath.length;
146
+ if (
147
+ path === entryPath ||
148
+ (path.length > entryLen &&
149
+ (path.charCodeAt(entryLen) === 46 ||
150
+ path.charCodeAt(entryLen) === 91) &&
151
+ path.startsWith(entryPath))
152
+ ) {
153
+ if (entryLen > longestLength) {
154
+ longestMatch = entry;
155
+ longestLength = entryLen;
156
+ }
157
+ }
158
+ }
159
+
160
+ return longestMatch ?? rootFallback;
147
161
  }
148
162
 
149
163
  export function requiredFields(instrument: TypedInstrument): string[] {
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ export {
12
12
  type HsxEffectKind,
13
13
  type UdlCostManifest,
14
14
  type UdlCostTable,
15
+ type UdlCostTables,
15
16
  } from "./cost.ts";
16
17
  export {
17
18
  hsxDiagnostics,
@@ -27,7 +28,7 @@ export {
27
28
  type ProgramEntryOverrideValue,
28
29
  } from "./entry-overrides.ts";
29
30
  export { type OriginMapEntry } from "./emit.ts";
30
- export { lowerGeneralProgram } from "./emit.ts";
31
+ export { lowerGeneralProgram, originForUdlPath } from "./emit.ts";
31
32
  export { parseProgram } from "./parse.ts";
32
33
  export { checkGeneralProgram, type GeneralCheckOptions } from "./typecheck.ts";
33
34
  export {
package/src/modules.ts CHANGED
@@ -327,7 +327,13 @@ function declarationReferences(decl: Decl): {
327
327
  if (decl.kind === "type") collectExprNames(decl.value, names, ports);
328
328
  if (decl.kind === "party" && decl.attrs)
329
329
  collectExprNames(decl.attrs, names, ports);
330
- if (decl.kind === "port") collectExprNames(decl.body, names, ports);
330
+ if (decl.kind === "port") {
331
+ collectExprNames(decl.body, names, ports);
332
+ const allowed = decl.body.entries.find(
333
+ (entry) => entry.key.name === "allowed",
334
+ );
335
+ if (allowed) collectApplicationPartyNames(allowed.value, parties);
336
+ }
331
337
  if (decl.kind === "instrument_apply" || isApplicationScopeDecl(decl)) {
332
338
  names.delete(decl.name.name);
333
339
  parties.delete(decl.name.name);
@@ -431,13 +437,13 @@ function standardSources(
431
437
  specifier: string,
432
438
  names: readonly string[],
433
439
  ): ModuleSource[] {
434
- if (specifier !== "std/settlements") return [];
440
+ if (specifier !== "std/money_flows") return [];
435
441
  return names.flatMap((name) => {
436
442
  const source = standardLibrary.source(specifier, name);
437
443
  return source !== undefined
438
444
  ? [
439
445
  {
440
- name: `std.settlements.${name}`,
446
+ name: `std.money_flows.${name}`,
441
447
  source,
442
448
  },
443
449
  ]
package/src/parse.ts CHANGED
@@ -367,7 +367,7 @@ class Parser {
367
367
  if (
368
368
  !this.expectPunct(
369
369
  "{",
370
- 'import lists its names in braces: import { held_payment } from "std/settlements"',
370
+ 'import lists its names in braces: import { held_payment } from "std/money_flows"',
371
371
  )
372
372
  ) {
373
373
  return undefined;
@@ -385,7 +385,7 @@ class Parser {
385
385
  if (!this.at("keyword") || this.peek().text !== "from") {
386
386
  this.error(
387
387
  this.peek().span,
388
- 'import needs from: import { held_payment } from "std/settlements"',
388
+ 'import needs from: import { held_payment } from "std/money_flows"',
389
389
  );
390
390
  return undefined;
391
391
  }
@@ -393,7 +393,7 @@ class Parser {
393
393
  if (!this.at("string")) {
394
394
  this.error(
395
395
  this.peek().span,
396
- 'the import source must be a quoted module name like "std/settlements"',
396
+ 'the import source must be a quoted module name like "std/money_flows"',
397
397
  );
398
398
  return undefined;
399
399
  }