@hyperscale0/hsx 2.1.1 → 2.2.1

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 (110) hide show
  1. package/CHANGELOG.md +38 -7
  2. package/dist/src/cli.d.ts +1 -1
  3. package/dist/src/cli.d.ts.map +1 -1
  4. package/dist/src/cli.js +39 -5
  5. package/dist/src/cli.js.map +1 -1
  6. package/dist/src/compile.d.ts +0 -1
  7. package/dist/src/compile.d.ts.map +1 -1
  8. package/dist/src/compile.js +15 -1
  9. package/dist/src/compile.js.map +1 -1
  10. package/dist/src/cost.d.ts +3 -3
  11. package/dist/src/cost.d.ts.map +1 -1
  12. package/dist/src/cost.js +22 -12
  13. package/dist/src/cost.js.map +1 -1
  14. package/dist/src/parse.d.ts.map +1 -1
  15. package/dist/src/parse.js +10 -14
  16. package/dist/src/parse.js.map +1 -1
  17. package/dist/src/std-bundle.d.ts.map +1 -1
  18. package/dist/src/std-bundle.js +20 -84
  19. package/dist/src/std-bundle.js.map +1 -1
  20. package/dist/src/typecheck.d.ts.map +1 -1
  21. package/dist/src/typecheck.js +171 -39
  22. package/dist/src/typecheck.js.map +1 -1
  23. package/dist/src/version.d.ts +1 -1
  24. package/dist/src/version.js +1 -1
  25. package/docs/README.md +4 -2
  26. package/docs/guide/01-first-program.md +1 -1
  27. package/docs/guide/03-instruments.md +14 -1
  28. package/docs/guide/06-schedules.md +5 -1
  29. package/docs/guide/08-writing-a-module.md +48 -0
  30. package/docs/llms-full.txt +1966 -422
  31. package/docs/llms.txt +3 -4
  32. package/docs/reference/cli.md +6 -5
  33. package/docs/reference/diagnostics.md +9 -9
  34. package/docs/reference/grammar.md +2 -3
  35. package/docs/reference/std/advance.md +94 -17
  36. package/docs/reference/std/cancellable_booking.md +138 -17
  37. package/docs/reference/std/captured_payment.md +96 -24
  38. package/docs/reference/std/conditional_disbursement.md +82 -14
  39. package/docs/reference/std/credit_facility.md +89 -16
  40. package/docs/reference/std/held_payment.md +155 -55
  41. package/docs/reference/std/instant_transfer.md +79 -12
  42. package/docs/reference/std/metered.md +71 -9
  43. package/docs/reference/std/pooled_split.md +76 -7
  44. package/docs/reference/std/premium_forward.md +100 -20
  45. package/docs/reference/std/reconciled_payout.md +84 -14
  46. package/docs/reference/std/rotating_pool.md +112 -24
  47. package/docs/reference/std/scheduled.md +117 -32
  48. package/docs/reference/std/security_deposit.md +119 -27
  49. package/docs/reference/std/settlement_batch.md +105 -24
  50. package/docs/reference/std/swap.md +113 -26
  51. package/docs/reference/std/threshold_pool.md +120 -29
  52. package/docs/reference/std/weighted_distribution.md +117 -21
  53. package/docs/reference/types.md +39 -15
  54. package/docs/reference/udl-output.md +6 -6
  55. package/examples/01-first-program/README.md +1 -1
  56. package/examples/{02-imports-and-archetypes → 02-imports-and-modules}/README.md +1 -1
  57. package/examples/{02-imports-and-archetypes → 02-imports-and-modules}/photo-booth.hsx +1 -1
  58. package/examples/04-complete-product/README.md +1 -1
  59. package/examples/05-authored-instrument/README.md +5 -0
  60. package/examples/05-authored-instrument/payment.hsx +37 -0
  61. package/examples/05-watch-club/watch-club.hsx +0 -1
  62. package/examples/README.md +1 -2
  63. package/examples/advance/advance.udl +29 -6
  64. package/examples/cancellable_booking/cancellable_booking.udl +19 -0
  65. package/examples/captured_payment/captured_payment.hsx +0 -4
  66. package/examples/captured_payment/captured_payment.udl +6 -0
  67. package/examples/conditional_disbursement/conditional_disbursement.hsx +0 -2
  68. package/examples/conditional_disbursement/conditional_disbursement.udl +4 -0
  69. package/examples/cost-table.json +136 -8
  70. package/examples/credit_facility/credit_facility.hsx +0 -3
  71. package/examples/credit_facility/credit_facility.udl +17 -1
  72. package/examples/held_payment/held_payment.udl +16 -0
  73. package/examples/instant_transfer/instant_transfer.udl +6 -0
  74. package/examples/metered/metered.udl +5 -1
  75. package/examples/pooled_split/pooled_split.udl +4 -0
  76. package/examples/premium_forward/premium_forward.udl +4 -0
  77. package/examples/reconciled_payout/reconciled_payout.udl +7 -0
  78. package/examples/rotating_pool/rotating_pool.udl +8 -0
  79. package/examples/scheduled/scheduled.udl +18 -3
  80. package/examples/security_deposit/security_deposit.udl +13 -0
  81. package/examples/settlement_batch/settlement_batch.udl +4 -0
  82. package/examples/swap/swap.udl +10 -0
  83. package/examples/threshold_pool/threshold_pool.udl +11 -0
  84. package/examples/weighted_distribution/weighted_distribution.udl +9 -0
  85. package/package.json +10 -10
  86. package/skills/hsx/SKILL.md +2 -36
  87. package/src/cli.ts +41 -5
  88. package/src/compile.ts +14 -6
  89. package/src/cost.ts +13 -16
  90. package/src/parse.ts +15 -10
  91. package/src/std-bundle.ts +21 -88
  92. package/src/typecheck.ts +207 -34
  93. package/src/version.ts +1 -1
  94. package/std/SEMANTICS.md +33 -128
  95. package/std/money_flows/advance.hsx +39 -26
  96. package/std/money_flows/cancellable_booking.hsx +275 -8
  97. package/std/money_flows/captured_payment.hsx +1 -9
  98. package/std/money_flows/conditional_disbursement.hsx +0 -6
  99. package/std/money_flows/credit_facility.hsx +0 -9
  100. package/std/money_flows/held_payment.hsx +39 -1
  101. package/std/money_flows/index.hsx +2 -1
  102. package/std/money_flows/metered.hsx +1 -5
  103. package/std/money_flows/scheduled.hsx +29 -119
  104. package/std/money_flows/threshold_pool.hsx +40 -5
  105. package/std/money_flows/weighted_distribution.hsx +47 -5
  106. package/docs/reference/std/recurring_collection.md +0 -25
  107. package/examples/recurring_collection/README.md +0 -3
  108. package/examples/recurring_collection/recurring_collection.hsx +0 -21
  109. package/examples/recurring_collection/recurring_collection.udl +0 -1135
  110. package/std/money_flows/recurring_collection.hsx +0 -72
package/src/typecheck.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import {
2
+ fixedIsoDurationMs,
3
+ referencedUdlInstrumentIds,
2
4
  deriveUdlActionEffects,
3
5
  resolveUdlActionPlans,
4
6
  udlDocumentSchema,
@@ -453,6 +455,28 @@ export function checkGeneralProgram(
453
455
  );
454
456
  }
455
457
 
458
+ const prefixByInstrument = new Map<string, string>();
459
+ if (options.publishedCatalog) {
460
+ for (const inst of options.publishedCatalog.instruments) {
461
+ if (inst.idPrefix) {
462
+ prefixByInstrument.set(inst.id, inst.idPrefix);
463
+ }
464
+ }
465
+ }
466
+ for (const [name, decl] of templates) {
467
+ const prefix =
468
+ stringSlot(decl.body, "idPrefix", "id_prefix") ?? prefixFor(name);
469
+ prefixByInstrument.set(name, prefix);
470
+ }
471
+ for (const candidate of allocated) {
472
+ const prefix =
473
+ stringSlot(candidate.body, "idPrefix", "id_prefix") ??
474
+ prefixFor(candidate.name.name);
475
+ prefixByInstrument.set(candidate.name.name, prefix);
476
+ }
477
+ const resolvePrefix = (target: string): string | undefined =>
478
+ prefixByInstrument.get(target);
479
+
456
480
  const instruments: TypedInstrument[] = [];
457
481
  const ids = new Set<string>();
458
482
  for (const candidate of allocated) {
@@ -483,6 +507,7 @@ export function checkGeneralProgram(
483
507
  candidate.ports,
484
508
  candidate.aliases,
485
509
  diagnostics,
510
+ resolvePrefix,
486
511
  );
487
512
  if (checked) instruments.push(checked);
488
513
  }
@@ -499,7 +524,11 @@ export function checkGeneralProgram(
499
524
  );
500
525
  }
501
526
 
502
- crossInstrumentReferenceDiagnostics(instruments, diagnostics);
527
+ crossInstrumentReferenceDiagnostics(
528
+ instruments,
529
+ diagnostics,
530
+ new Set(templates.keys()),
531
+ );
503
532
 
504
533
  if (
505
534
  header &&
@@ -656,6 +685,15 @@ function checkPublishedProgram(
656
685
  );
657
686
  continue;
658
687
  }
688
+ if (action?.engineOwned || authoredAction?.slots.engineOwned === true) {
689
+ report(
690
+ "HSX1021",
691
+ exposure.span,
692
+ `${target} is engine-owned`,
693
+ "expose a caller action instead",
694
+ );
695
+ continue;
696
+ }
659
697
  if (!CAMEL_CASE.test(exposure.publicName.name)) {
660
698
  report(
661
699
  "HSX1020",
@@ -843,24 +881,7 @@ function publishedInstrumentDependencies(
843
881
  instrument: UdlInstrument,
844
882
  idByPrefix: ReadonlyMap<string, string>,
845
883
  ): readonly string[] {
846
- const found = new Set<string>();
847
- const visit = (value: unknown): void => {
848
- if (Array.isArray(value)) {
849
- value.forEach(visit);
850
- return;
851
- }
852
- if (!value || typeof value !== "object") return;
853
- for (const [key, child] of Object.entries(value)) {
854
- if (
855
- (key === "instrumentId" || key === "childInstrumentId") &&
856
- typeof child === "string"
857
- ) {
858
- found.add(child);
859
- }
860
- visit(child);
861
- }
862
- };
863
- visit(instrument);
884
+ const found = new Set(referencedUdlInstrumentIds(instrument));
864
885
  for (const field of Object.values(instrument.fields)) {
865
886
  const prefix =
866
887
  typeof field.pattern === "string"
@@ -1110,7 +1131,6 @@ const applicationMetadataKeys = new Set([
1110
1131
  "action",
1111
1132
  "agent_description",
1112
1133
  "description",
1113
- "journeys",
1114
1134
  "nav",
1115
1135
  "navigation",
1116
1136
  "summary",
@@ -1423,7 +1443,12 @@ function inferSettlementTypeArgument(
1423
1443
  typeParameter: string,
1424
1444
  ): Expr | undefined {
1425
1445
  for (const parameter of template.parameters) {
1426
- const parameterType = parameter.type;
1446
+ const declaredType = parameter.type;
1447
+ const parameterType =
1448
+ declaredType.kind === "type_apply" &&
1449
+ declaredType.callee.name === "optional"
1450
+ ? (declaredType.args[0] ?? declaredType)
1451
+ : declaredType;
1427
1452
  if (
1428
1453
  parameterType.kind !== "type_apply" ||
1429
1454
  parameterType.callee.name !== "money" ||
@@ -1727,9 +1752,53 @@ function instantiate(
1727
1752
  values,
1728
1753
  portDependencies,
1729
1754
  );
1755
+ const binding: Entry = {
1756
+ key: { kind: "ident", name: "template_binding", span: application.span },
1757
+ qualifiers: [],
1758
+ span: application.span,
1759
+ value: {
1760
+ kind: "block",
1761
+ span: application.span,
1762
+ entries: [
1763
+ {
1764
+ key: { kind: "ident", name: "id", span: application.span },
1765
+ qualifiers: [],
1766
+ span: application.span,
1767
+ value: {
1768
+ kind: "string",
1769
+ value: template.name.name,
1770
+ span: application.span,
1771
+ },
1772
+ },
1773
+ {
1774
+ key: { kind: "ident", name: "parameters", span: application.span },
1775
+ qualifiers: [],
1776
+ span: application.span,
1777
+ value: {
1778
+ kind: "block",
1779
+ span: application.span,
1780
+ entries: resolvedArgs.flatMap(({ parameter }) => {
1781
+ const value = values.get(parameter.name.name);
1782
+ return value &&
1783
+ ["string", "number", "boolean"].includes(value.kind)
1784
+ ? [
1785
+ {
1786
+ key: parameter.name,
1787
+ qualifiers: [],
1788
+ span: parameter.span,
1789
+ value,
1790
+ },
1791
+ ]
1792
+ : [];
1793
+ }),
1794
+ },
1795
+ },
1796
+ ],
1797
+ },
1798
+ };
1730
1799
  return {
1731
1800
  ...body,
1732
- entries: body.entries.map((row) => {
1801
+ entries: [...body.entries, binding].map((row) => {
1733
1802
  if (row.key.name !== "fields" || row.value.kind !== "block") return row;
1734
1803
  return {
1735
1804
  ...row,
@@ -2305,9 +2374,16 @@ function substituteExpr(expr: Expr, values: ReadonlyMap<string, Expr>): Expr {
2305
2374
  }
2306
2375
  : {}),
2307
2376
  key: substituteName(entry.key, nestedValues),
2308
- qualifiers: entry.qualifiers.map((qualifier, index) => {
2377
+ qualifiers: entry.qualifiers.flatMap((qualifier, index) => {
2309
2378
  const value = nestedValues.get(qualifier.name);
2310
2379
  const fixed = fixedQualifier(entry.key.name, qualifier.name, index);
2380
+ if (
2381
+ entry.key.name === "on" &&
2382
+ !fixed &&
2383
+ index > 0 &&
2384
+ value?.kind === "list"
2385
+ )
2386
+ return value.items.map((item) => nameFromValue(qualifier, item));
2311
2387
  return !fixed && value
2312
2388
  ? nameFromValue(qualifier, value)
2313
2389
  : substituteName(qualifier, nestedValues);
@@ -2804,6 +2880,17 @@ function evaluateCompileTimeCall(
2804
2880
  : undefined;
2805
2881
  case "basis_points":
2806
2882
  return first?.kind === "percent" ? numeric(first.bps) : undefined;
2883
+ case "shift_date": {
2884
+ const instant = first ? Date.parse(nameText(first)) : NaN;
2885
+ const offset = second ? fixedIsoDurationMs(nameText(second)) : null;
2886
+ if (!Number.isFinite(instant) || offset === null)
2887
+ return text(NONE_SENTINEL);
2888
+ const direction = third && nameText(third) === "before" ? -1 : 1;
2889
+ const shifted = new Date(instant + direction * offset);
2890
+ return Number.isFinite(shifted.getTime())
2891
+ ? text(shifted.toISOString())
2892
+ : text(NONE_SENTINEL);
2893
+ }
2807
2894
  case "names": {
2808
2895
  const count = number(second);
2809
2896
  if (!first || count === undefined || count < 0 || !third)
@@ -2826,6 +2913,15 @@ function evaluateCompileTimeCall(
2826
2913
  span: expr.span,
2827
2914
  }
2828
2915
  : undefined;
2916
+ case "without":
2917
+ return first?.kind === "list" && second
2918
+ ? {
2919
+ ...first,
2920
+ items: first.items.filter(
2921
+ (item) => nameText(item) !== nameText(second),
2922
+ ),
2923
+ }
2924
+ : undefined;
2829
2925
  case "concat_lists":
2830
2926
  return expr.args.every((arg) => arg.kind === "list")
2831
2927
  ? {
@@ -2992,9 +3088,16 @@ function expandComprehensions(
2992
3088
  const substituted: Entry = {
2993
3089
  ...row,
2994
3090
  key: substituteName(row.key, locals),
2995
- qualifiers: row.qualifiers.map((qualifier, index) => {
3091
+ qualifiers: row.qualifiers.flatMap((qualifier, index) => {
2996
3092
  const value = locals.get(qualifier.name);
2997
3093
  const fixed = fixedQualifier(row.key.name, qualifier.name, index);
3094
+ if (
3095
+ row.key.name === "on" &&
3096
+ !fixed &&
3097
+ index > 0 &&
3098
+ value?.kind === "list"
3099
+ )
3100
+ return value.items.map((item) => nameFromValue(qualifier, item));
2998
3101
  return !fixed && value
2999
3102
  ? nameFromValue(qualifier, value)
3000
3103
  : substituteName(qualifier, locals);
@@ -3077,6 +3180,7 @@ function checkInstrument(
3077
3180
  ports: ReadonlyMap<string, readonly PortDecl[]>,
3078
3181
  aliases: ReadonlyMap<string, Expr>,
3079
3182
  diagnostics: GeneralDiagnostic[],
3183
+ resolvePrefix?: (target: string) => string | undefined,
3080
3184
  ): TypedInstrument | undefined {
3081
3185
  if (!SNAKE_CASE.test(name.name)) {
3082
3186
  diagnostics.push({
@@ -3090,7 +3194,7 @@ function checkInstrument(
3090
3194
  const fieldsEntry = entry(body, "fields");
3091
3195
  const fields =
3092
3196
  fieldsEntry?.value.kind === "block"
3093
- ? checkFields(fieldsEntry.value, aliases, diagnostics)
3197
+ ? checkFields(fieldsEntry.value, aliases, diagnostics, resolvePrefix)
3094
3198
  : [];
3095
3199
  if (!fieldsEntry) {
3096
3200
  diagnostics.push({
@@ -3597,6 +3701,7 @@ function checkFields(
3597
3701
  block: BlockExpr,
3598
3702
  aliases: ReadonlyMap<string, Expr>,
3599
3703
  diagnostics: GeneralDiagnostic[],
3704
+ resolvePrefix?: (target: string) => string | undefined,
3600
3705
  ): TypedField[] {
3601
3706
  const result: TypedField[] = [];
3602
3707
  const names = new Set<string>();
@@ -3622,7 +3727,7 @@ function checkFields(
3622
3727
  continue;
3623
3728
  }
3624
3729
  names.add(name);
3625
- result.push(lowerField(row, aliases, diagnostics));
3730
+ result.push(lowerField(row, aliases, diagnostics, resolvePrefix));
3626
3731
  }
3627
3732
  return result;
3628
3733
  }
@@ -3631,6 +3736,7 @@ function lowerField(
3631
3736
  row: Entry,
3632
3737
  aliases: ReadonlyMap<string, Expr>,
3633
3738
  diagnostics: GeneralDiagnostic[],
3739
+ resolvePrefix?: (target: string) => string | undefined,
3634
3740
  ): TypedField {
3635
3741
  const name = camel(row.key.name);
3636
3742
  let typeExpr = row.value;
@@ -3692,6 +3798,21 @@ function lowerField(
3692
3798
  severity: "error",
3693
3799
  span: declaredSpan,
3694
3800
  });
3801
+ } else if (type.kind === "ref" && type.target) {
3802
+ const prefix = resolvePrefix?.(type.target);
3803
+ if (!extra.pattern) {
3804
+ if (prefix) {
3805
+ extra.pattern = `^${prefix}_(sandbox|live)_[a-z0-9]{8,64}$`;
3806
+ } else if (resolvePrefix !== undefined) {
3807
+ diagnostics.push({
3808
+ code: "HSX1007",
3809
+ fix: "reference an instrument declared in this program, imported from std, or published in the catalogue",
3810
+ message: `field ${name} references unknown instrument ${type.target}`,
3811
+ severity: "error",
3812
+ span: declaredSpan,
3813
+ });
3814
+ }
3815
+ }
3695
3816
  }
3696
3817
 
3697
3818
  const structuralType = Object.hasOwn(extra, "items")
@@ -4174,9 +4295,38 @@ function bindFieldReferences(
4174
4295
  ): void {
4175
4296
  const externalFieldSlot = (path: readonly string[]): boolean => {
4176
4297
  const key = path.at(-1);
4298
+ const input =
4299
+ slots.input !== undefined && isJsonObject(slots.input)
4300
+ ? slots.input
4301
+ : undefined;
4302
+ const allocation =
4303
+ slots.allocate !== undefined && isJsonObject(slots.allocate)
4304
+ ? slots.allocate
4305
+ : undefined;
4306
+ const allocationOperand =
4307
+ path.includes("allocate") &&
4308
+ ["amountField", "sourceAccountField", "paymentIdentityField"].includes(
4309
+ key ?? "",
4310
+ );
4177
4311
  return (
4312
+ (allocationOperand &&
4313
+ Array.isArray(input?.required) &&
4314
+ allocation !== undefined &&
4315
+ input.required.includes(allocation[key!]!)) ||
4316
+ (path.includes("funding") &&
4317
+ [
4318
+ "ticketRefField",
4319
+ "ticketAmountField",
4320
+ "ticketInvestorField",
4321
+ "ticketAccountField",
4322
+ ].includes(key ?? "")) ||
4323
+ (path.includes("requiresExposure") && key === "minimumField") ||
4178
4324
  (path.includes("requiresAggregate") && key === "refField") ||
4179
- (path.includes("requiresAggregate") && key === "amountField") ||
4325
+ (path.includes("requiresAggregate") &&
4326
+ ["amountField", "dateField", "positionField"].includes(key ?? "")) ||
4327
+ (path.includes("requiresAggregate") &&
4328
+ path.includes("dueBefore") &&
4329
+ key === "field") ||
4180
4330
  (path.includes("signedSum") && key === "refField") ||
4181
4331
  (path.includes("signedSum") && key === "amountField") ||
4182
4332
  (path.includes("remainder") && key === "refField") ||
@@ -4220,6 +4370,7 @@ function bindFieldReferences(
4220
4370
  function crossInstrumentReferenceDiagnostics(
4221
4371
  instruments: readonly TypedInstrument[],
4222
4372
  diagnostics: GeneralDiagnostic[],
4373
+ knownTargets?: ReadonlySet<string>,
4223
4374
  ): void {
4224
4375
  const byId = new Map(
4225
4376
  instruments.map((instrument) => [instrument.id, instrument]),
@@ -4254,7 +4405,10 @@ function crossInstrumentReferenceDiagnostics(
4254
4405
  for (const owner of instruments) {
4255
4406
  for (const field of owner.fields) {
4256
4407
  if (field.type.kind !== "ref" || !field.type.target) continue;
4257
- if (!byId.has(field.type.target)) {
4408
+ if (
4409
+ !byId.has(field.type.target) &&
4410
+ !knownTargets?.has(field.type.target)
4411
+ ) {
4258
4412
  report(
4259
4413
  owner,
4260
4414
  field.origin,
@@ -4367,6 +4521,23 @@ function crossInstrumentReferenceDiagnostics(
4367
4521
  return;
4368
4522
  }
4369
4523
  const refField = relation.refField;
4524
+ const anchorName =
4525
+ relation.anchorField ??
4526
+ (relation.over === "siblings" ? refField : undefined);
4527
+ const anchor =
4528
+ typeof anchorName === "string" ? fieldIn(owner, anchorName) : undefined;
4529
+ const expectedTarget =
4530
+ anchor?.type.kind === "ref" ? anchor.type.target : owner.id;
4531
+ if (
4532
+ typeof anchorName === "string" &&
4533
+ (!anchor || anchor.type.kind !== "ref")
4534
+ )
4535
+ report(
4536
+ owner,
4537
+ action.origin,
4538
+ `aggregate anchor ${anchorName} is not a reference`,
4539
+ "declare the anchor as a typed parent reference",
4540
+ );
4370
4541
  if (typeof refField === "string") {
4371
4542
  const field = fieldIn(target, refField);
4372
4543
  if (!field) {
@@ -4374,17 +4545,18 @@ function crossInstrumentReferenceDiagnostics(
4374
4545
  owner,
4375
4546
  action.origin,
4376
4547
  `${targetId}.${refField} does not exist`,
4377
- `declare ${refField} on ${targetId} as ref<${owner.id}> or correct the clause`,
4548
+ `declare ${refField} on ${targetId} as ref<${expectedTarget}> or correct the clause`,
4378
4549
  );
4379
4550
  } else if (
4380
4551
  field.type.kind !== "ref" ||
4381
- (field.type.target !== undefined && field.type.target !== owner.id)
4552
+ (field.type.target !== undefined &&
4553
+ field.type.target !== expectedTarget)
4382
4554
  ) {
4383
4555
  report(
4384
4556
  owner,
4385
4557
  action.origin,
4386
- `${targetId}.${refField} has ${typeWords(field.type)}; this relation needs ref<${owner.id}>`,
4387
- `change ${targetId}.${refField} to ref<${owner.id}>`,
4558
+ `${targetId}.${refField} has ${typeWords(field.type)}; this relation needs ref<${expectedTarget}>`,
4559
+ `change ${targetId}.${refField} to ref<${expectedTarget}>`,
4388
4560
  );
4389
4561
  }
4390
4562
  }
@@ -4408,12 +4580,13 @@ function crossInstrumentReferenceDiagnostics(
4408
4580
  const targetField = check?.targetField;
4409
4581
  if (
4410
4582
  typeof targetField === "string" &&
4411
- fieldIn(owner, targetField)?.type.kind !== "money"
4583
+ fieldIn(owner, targetField)?.type.kind !==
4584
+ (check?.kind === "count_at_least" ? "integer" : "money")
4412
4585
  ) {
4413
4586
  report(
4414
4587
  owner,
4415
4588
  action.origin,
4416
- `${owner.id}.${targetField} is not declared money`,
4589
+ `${owner.id}.${targetField} is not declared ${check?.kind === "count_at_least" ? "integer" : "money"}`,
4417
4590
  `declare ${owner.id}.${targetField} as money<C> or correct the aggregate target`,
4418
4591
  );
4419
4592
  }
package/src/version.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /** Package version reported by the command line. */
2
- export const HSX_VERSION = "2.1.1";
2
+ export const HSX_VERSION = "2.2.1";
3
3
 
4
4
  /** Canonical UDL contract version emitted by this compiler. */
5
5
  export const HSX_TARGET_UDL_VERSION = 1;
package/std/SEMANTICS.md CHANGED
@@ -1,139 +1,44 @@
1
- # Settlement semantics frozen before legacy teardown
1
+ # Standard library money flows semantics
2
2
 
3
- This note records the behavior that the former archetype branches emitted on
4
- 2026-09-01. The in-place compiler family specs under `test/` are the semantic
5
- authority. This note is the human porting index.
3
+ This document records the runtime semantics and settlement mechanics of standard library modules under `std.money_flows`. The compiler and its unit specifications under `test/` remain the authoritative implementation reference.
6
4
 
7
- ## Direct and held payments
5
+ ## Direct checkout and card
8
6
 
9
- - `instant_transfer` stores the amount, currency, payer, and payee. Create pays
10
- through one or more ordered pieces and ends in `paid`. A payer fee is an
11
- `on_top` move. A payee fee is carved from the base amount. Exact, basis-point,
12
- and tiered rules produce fee fields and `feeRules`. The finest common
13
- refinement of all fee cuts produces piece fields and one partition. Floor
14
- rounding sends the remainder to the named non-fee recipient. A derived
15
- amount adds a floor percentage field, `derivedAmounts`, a platform party, and
16
- its own transfer piece.
17
- - `held_payment` reserves every refined piece before release. It posts release
18
- pieces, voids cancellation pieces, and keeps payer service fees outside the
19
- held principal. Release and cancellation fee sides share the finest common
20
- partition, so each stored piece has one release recipient and one cancel
21
- recipient. States track each funding, release, cancellation, and abandonment
22
- step. Abandonment refunds every still-held piece. A deadline release and a
23
- caller decision release carry the named date or port clause. Retention is the
24
- same form with one held piece, a release deadline, and cancellation back to
25
- the contractor. Whole-amount mode funds the principal and on-top fee in one
26
- action, then releases or cancels the principal whole. A condition and date
27
- can coexist, with separate early and scheduled release actions.
28
- - `captured_payment` stores authorization total, reserve and reversal dates,
29
- and both party accounts. It moves through `created`, `authorized`, optional
30
- `partially_captured`, `settled`, `voided`, `expired`, `corrected`, or
31
- `reversed`. Authorize reserves custody. Capture and capture-more consume the
32
- authorized balance according to the declared mode. Settle posts the captured
33
- amount. Void and expiry release the reserve. Correction and external reversal
34
- use their declared ports and windows. Derived fees use the same floor,
35
- partition, bearer, and position rules as direct payment.
36
- - `cancellable_booking` holds the booking amount in escrow away from guest and
37
- host until the booking ends or is cancelled. Cancellation quotes a penalty
38
- against the time remaining before the start date, frozen with offer life.
39
- Lifecycle moves through `created`, `held`, `cancellation_quoted`, `canceled`,
40
- `settled`, and `completed`. Penalty retention moves penalty funds to host and
41
- remainder to guest; completion releases full amount to host.
42
- - `premium_forward` holds premium pieces, then forwards the net pieces after a
43
- bind decision. It can abandon unbound custody. Policy reference, renewal due,
44
- endorsement evidence, and lapse actions appear only when declared. Its fee
45
- partition and floor remainder rules match `held_payment`.
7
+ - `instant_transfer` stores the amount, currency, payer, and payee. Action `create` opens the payment record without moving funds. Action `pay_piece_1` and subsequent piece actions execute the money movements, transitioning to state `paid`. A payer fee executes as an `on_top` move. A payee fee carves out of the base amount. Exact, basis-point, and tiered rules generate fee fields and `feeRules`. The finest common refinement of all fee cuts produces piece fields and one partition. Floor rounding sends minor-unit remainder to the designated non-fee recipient. A derived amount adds a floor percentage field, `derivedAmounts`, a platform party account, and its own transfer piece.
8
+ - `captured_payment` stores authorization total, reserve and reversal dates, and party accounts. The instrument moves through `created`, `authorized`, optional `partially_captured`, `settled`, `voided`, `expired`, `corrected`, or `reversed`. Action `authorize` reserves custody on the payer balance. Actions `capture` and `capture_more` consume the authorized balance using partial posts. Action `settle` posts the captured amount. Actions `void` and `expire` release the reserve. Actions for payee correction and external reversal use their declared condition ports and deadline windows. Derived fees use the same floor, partition, bearer, and position rules as direct payment. Four unused signature knobs (`capture_mode`, `correction_mode`, `negative_position`, `timeout`) are pruned.
46
9
 
47
- ## Decisions and credit
10
+ ## Custody and escrow
48
11
 
49
- - `conditional_disbursement` emits a submitted parent with cap, currency,
50
- source, destination, and the decision port. Denial terminates the parent. An
51
- approved-amount child stores a runtime-bounded amount and a parent reference.
52
- The child captures the port input, moves through `created`, `approved`, and
53
- `paid`, and refuses reopening. Recovery is a separate transfer.
54
- - `advance` has two forms. A carved advance references a held settlement and
55
- its recourse settlement, disburses once, and settles from the referenced
56
- release without minting new value. A scheduled advance stores advance,
57
- fee, repayable total, first due date, and one repayment field per fixed
58
- installment. It partitions repayments to the repayable total and advances
59
- before collecting each due installment.
60
- - `credit_facility` stores lender, borrower, draw destination, limit, currency,
61
- and expiry. The facility can freeze and close. Each draw is a generated child
62
- with amount, facility reference, and obligation reference. Draw admission
63
- checks the facility state and aggregate limit. Resolution waits for the
64
- referenced obligation state.
12
+ - `held_payment` reserves every refined piece in a dedicated escrow account before release. It posts release pieces, voids cancellation pieces, and keeps payer service fees outside the held principal. Release and cancellation fee sides share the finest common partition, so each stored piece has one release recipient and one cancel recipient. States track funding, release, cancellation, and abandonment steps. Abandonment refunds every still-held piece. A deadline release and a caller decision release carry the named date or port clause. Retention forms reserve one held piece with a release deadline and cancellation back to the contractor. Whole-amount mode funds principal and on-top fees in one action, then releases or cancels the principal whole. Quoted cancellation uses `cancel_charge_bps` and `cancel_offer_life` to freeze penalty quotes before confirm and retain actions.
13
+ - `cancellable_booking` holds booking funds in escrow away from guest and host until the booking ends or cancels. Cancellation quotes a penalty against the time remaining before `starts_at`, frozen for `offer_life`. Lifecycle moves through `created`, `held`, `cancellation_quoted`, `canceled`, `settled`, and `completed`. Penalty retention moves penalty funds to host and remainder to guest; completion releases full amount to host.
14
+ - `security_deposit` reserves the full deposit amount in escrow. A claim port posts either the whole hold or an externally decided amount bounded by the hold. The explicit remainder returns to the payer. A return port voids the whole hold. The deadline form adds machine expiry and unfunded cancellation.
15
+ - `premium_forward` holds premium pieces in escrow, then forwards net pieces after a carrier bind decision. It can abandon unbound custody. Policy reference, renewal due, endorsement evidence, and lapse actions activate when declared. Fee partition and floor remainder rules match `held_payment`.
65
16
 
66
- ## Schedules and usage
17
+ ## Subscriptions, usage, and schedules
67
18
 
68
- - A finite `scheduled` transfer stores total, first due date, and one money
69
- field per installment. Floor division creates equal pieces and gives the
70
- final field the remainder. States and actions unroll in order. Each action
71
- carries its due rule and cumulative duration offset.
72
- - Scheduled obligation mode emits a parent plus one generated payment child per
73
- installment. The parent stores principal, delinquency dates, party accounts,
74
- partitions, and aggregate invariants. It supports draft approval, optional
75
- advance, fixed collection and delinquency actions, write-off, and completion.
76
- Each child binds parent fields, repays once, and can refund once.
77
- - `recurring_collection` with a finite count uses the scheduled construction.
78
- Open recurrence stores one amount and anchor, opens one period at a time,
79
- collects that period, and permits cancellation. It never unrolls an unbounded
80
- runtime loop.
81
- - `metered` stores one money field for each declared meter and a period end.
82
- Every charge moves directly from payer to payee. The instrument never accrues
83
- custody. Closing the period only changes state.
19
+ - `scheduled` executes calendar-anchored payment series between one payer and one payee across three operational modes:
20
+ 1. Finite installment plan: Stores total, first due date, and one money field per installment. The caller supplies each piece; a partition checks their sum, not equality. States and actions unroll in order with cumulative duration offsets.
21
+ 2. Open recurring subscription: Charges a recurring amount on interval `every` until an `until` port fires, opening one period liability at a time.
22
+ 3. Debt obligation: Emits a parent obligation plus one child payment instrument per installment. The parent stores principal, delinquency dates, party accounts, and aggregate invariants. Child instruments bind parent fields, repay against installment caps, and support full refunds. Finite obligation count generalization applies without grace periods.
23
+ - `metered` stores one money field for each declared meter in its rate card and a period end date. Each usage charge executes an immediate transfer directly from payer to payee without escrow custody. Closing the period transitions state to `closed` at `close_by` and makes further charges unreachable.
84
24
 
85
- ## Collections and distributions
25
+ ## Lending and credit
86
26
 
87
- - `pooled_split` stores the pool total, due date, and one share amount and
88
- account per recipient. Positive shares total exactly 10,000 basis points.
89
- Floor division assigns each share, and the named remainder recipient absorbs
90
- minor-unit residue. Funding and distribution actions unroll in roster order.
91
- - `weighted_distribution` emits an open parent and an entitlement child. The
92
- parent stores source, total, record date, and maximum recipient count. A port
93
- freezes the entitlement snapshot. Child rows store recipient, weight, source,
94
- currency, and parent reference. Largest-remainder distribution pays children
95
- once. Aggregate clauses cap and total the child set. Withholding is refused
96
- and correction creates a new distribution. Its `flat` form emits a single
97
- claim instead. The caller supplies typed pool, weight, and group references,
98
- their state gates, copy and match maps, and the pool amount path.
99
- - `threshold_pool` emits a pool parent and commitment child. The parent stores
100
- target, close date, maximum contributors, beneficiary, and currency. It moves
101
- through open, active, failed, or settled based on aggregate commitment gates.
102
- Each commitment can cancel before collection, collect into the round, or
103
- refund after failure. Its referenced-contribution form accepts a published
104
- contribution instrument, beneficiary account, and memo and emits no child.
105
- - `security_deposit` reserves the full amount. A claim port posts either the
106
- whole hold or a decided amount bounded by the hold. The explicit remainder
107
- returns to the payer. A return port voids the whole hold. The deadline form
108
- adds machine expiry and unfunded cancellation. Claim and return targets can
109
- name either party.
27
+ - `advance` provides upfront capital disbursed to a recipient and repaid through two distinct modes:
28
+ 1. Carved advance: References an active escrow hold and recourse instrument through `against`. It disburses once and settles from the referenced release without minting new value. In this mode, the `fee` parameter is ignored.
29
+ 2. Scheduled advance: Stores advance amount, fee amount, repayable total, first due date, and one repayment field per installment. A supplied `fee` derives feeAmount as a floor percentage of principal. The caller supplies principal, repayable total and repayment pieces, which partitions reconcile. `repayment_source` can differ from the capital recipient. `profit_to` splits each collection between principal returned to the funder and profit paid to its recipient. `dated: true` requires one stored signed due date per repayment. Equality, chronological ordering, partial servicing and arrears are not implied by those partitions.
30
+ - `credit_facility` stores lender, borrower, draw destination, credit limit, currency, and expiry date. The facility transitions across `active`, `frozen`, and `closed`. Each draw creates a child instrument with draw amount, facility reference, and obligation reference. Draw admission checks facility active state and aggregate child exposure against the limit. Closure requires all child draws to reach status `resolved`. Unused signature knobs (`availability_policy`, `expiry_policy`, `close_policy`) are pruned.
31
+ - `conditional_disbursement` emits a submitted parent instrument with cap, currency, source account, destination account, and decision port. Denial terminates the parent before child approvals. Child approval instruments store externally bounded amounts. Child actions capture port input, move through `created`, `approved`, and `paid`, and verify aggregate child exposure against the parent cap. Unused signature knobs (`reopen_policy`, `recovery_policy`) are pruned.
110
32
 
111
- ## Composition and unwind
33
+ ## Multi-party distributions and pooling
112
34
 
113
- - `swap` stores both side amounts, accounts, fees, currency, and optional
114
- clawback date. Funding is atomic across both sides. Release, settlement,
115
- cancellation, dispute, abandonment, and clawback preserve the two-sided
116
- conservation group. Side fees keep their declared bearer and position.
117
- `distinctParties` prevents self-dealing. Unwind refunds each side and applies
118
- the declared penalty tiers without changing the principal partition. Fixed
119
- side names, state order, action bindings, parked-state reasons, and ID prefix
120
- are general compile-time parameters.
121
- - `settlement_batch` emits a batch plus capture, credit-adjustment, and
122
- debit-adjustment children. The batch stores close time, settlement and payout
123
- accounts, beneficiary reference, and currency. Close freezes intake.
124
- Calculate signs capture, fee, and reversal entries. Approve rejects a negative
125
- position when configured. Instruct, acknowledge, and reconcile use the
126
- declared payout port. Child references and statuses define every aggregate
127
- and signed-sum input.
128
- - `rotating_pool` emits one parent plus one contribution child per fixed member.
129
- The parent unrolls active and ready states for every cycle and checks that each
130
- member row exists exactly once. Every child unrolls due, funded, defaulted,
131
- guaranteed, paid, and completed states for every cycle. Due offsets scale the
132
- fixed recurrence. A funded or guaranteed contribution pays the roster's fixed
133
- beneficiary for that cycle. The final close requires the escrow account to be
134
- drained. Its referenced-membership form delegates those member cycles to a
135
- published membership instrument and emits only the parent.
136
- - `reconciled_payout` instructs an external payout with a tolerance dial,
137
- waits for a matching bank debit statement line, and treats unmatched amounts
138
- at the settle date as break rows. Lifecycle moves through `created`,
139
- `instructed`, and `settled`.
35
+ - `pooled_split` stores pool total, due date, and one share amount and account per recipient. Positive shares total exactly 10,000 basis points. Floor division assigns each share, and the designated remainder recipient absorbs minor-unit residues. Funding and distribution actions execute in roster order.
36
+ - `weighted_distribution` emits a parent distribution instrument and child entitlement instruments. The parent stores source account, total amount, record date, and maximum recipient count. Child rows store recipient, weight, source, currency, and parent reference. A largest-remainder distribution algorithm pays children once. Aggregate clauses cap and total child records against the pool.
37
+ - `threshold_pool` emits a pool parent instrument and child commitment instruments. The parent stores target amount, close date, maximum contributors, beneficiary, and currency. It transitions through `open`, `active`, `failed`, or `settled` based on aggregate commitment gates. Each commitment can cancel before collection, collect into the pool, or refund after campaign failure.
38
+ - `rotating_pool` emits a parent pool instrument plus one contribution child instrument per member. The parent unrolls active and ready states for every cycle and checks that each member row exists exactly once. Member child instruments unroll due, funded, defaulted, guaranteed, paid, and completed states for every cycle. A funded or guaranteed contribution pays the fixed beneficiary for that cycle. Final closure requires the escrow balance to drain to zero.
39
+
40
+ ## Bilateral exchange, clearing, and reconciliation
41
+
42
+ - `swap` stores amounts, accounts, fees, currency, and optional clawback dates for both sides. Funding is atomic across both sides. Release, settlement, cancellation, dispute, abandonment, and clawback preserve the two-sided conservation group. Side fees retain their declared bearer and position. Unwind refunds each side and applies declared penalty tiers without altering principal partitions.
43
+ - `settlement_batch` emits a batch instrument alongside capture, credit-adjustment, and debit-adjustment child records. The batch stores close time, settlement and payout accounts, beneficiary reference, and currency. Closure freezes intake. Calculation applies signed arithmetic across capture, fee, and reversal entries. Actions instruct, acknowledge, and reconcile through the declared payout port.
44
+ - `reconciled_payout` instructs an external payout with a tolerance window, waits for a matching bank debit statement line, and tracks unmatched amounts at settlement date as break rows. Lifecycle transitions through `created`, `instructed`, and `settled`.