@hyperscale0/hsx 1.0.125 → 1.0.130

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +2 -2
  2. package/dist/src/cli.js +1 -1
  3. package/dist/src/cli.js.map +1 -1
  4. package/dist/src/compile.d.ts.map +1 -1
  5. package/dist/src/compile.js +155 -21
  6. package/dist/src/compile.js.map +1 -1
  7. package/dist/src/examples-bundle.js +9 -9
  8. package/dist/src/examples-bundle.js.map +1 -1
  9. package/dist/src/headers.d.ts +1 -1
  10. package/dist/src/headers.d.ts.map +1 -1
  11. package/dist/src/headers.js +1 -0
  12. package/dist/src/headers.js.map +1 -1
  13. package/dist/src/parse.js +4 -1
  14. package/dist/src/parse.js.map +1 -1
  15. package/dist/src/std-bundle.d.ts.map +1 -1
  16. package/dist/src/std-bundle.js +8 -7
  17. package/dist/src/std-bundle.js.map +1 -1
  18. package/docs/README.md +102 -50
  19. package/examples/cards.hsx +4 -0
  20. package/examples/collections.hsx +5 -1
  21. package/examples/cost-table.json +80 -56
  22. package/examples/financing.hsx +1 -1
  23. package/examples/insurance.hsx +5 -4
  24. package/examples/lending.hsx +1 -1
  25. package/examples/library.hsx +1 -1
  26. package/examples/rental-deposit.hsx +1 -1
  27. package/examples/reporting.hsx +1 -1
  28. package/examples/serviced.hsx +1 -1
  29. package/package.json +4 -4
  30. package/src/cli.ts +1 -1
  31. package/src/compile.ts +223 -27
  32. package/src/examples-bundle.ts +9 -9
  33. package/src/headers.ts +1 -0
  34. package/src/parse.ts +4 -1
  35. package/src/std-bundle.ts +8 -7
  36. package/std/booking.hsx +75 -0
  37. package/std/cards.hsx +5 -3
  38. package/std/escrow.hsx +35 -9
  39. package/std/financing.hsx +158 -36
  40. package/std/insurance.hsx +9 -5
  41. package/std/marketplace.hsx +3 -3
  42. package/std/money.hsx +3 -3
  43. package/std/wallet.hsx +20 -9
@@ -2,7 +2,6 @@
2
2
  program device_insurance "Device insurance"
3
3
  use insurance
4
4
  use money
5
- party inspector: staff role claims
6
5
 
7
6
  object device "Insured device" {
8
7
  fields { model: text, serialNumber: text, purchaseDate: date }
@@ -13,16 +12,18 @@ object device "Insured device" {
13
12
  economics pay { purpose: earning, sourceParty: owner }
14
13
  expose pay as pay_for_device
15
14
  }
16
- // Bind device_insurer to an ADL adapter before creating cover. Premiums net of commission go to its account.
15
+ // conformance_boundary is the sandbox insurer adapter; swap in the insurer's registered adapter to go live.
16
+ // Premiums net of commission go to its account. The default broker is the retailer, which keeps the commission.
17
+ // claim_limit is the sum insured, set per cover at creation.
17
18
  attach protection = insurance.cover {
18
19
  expose create as create_protection
19
- holder: owner, adapter: "device_insurer", commission: 10%, covers: purchase
20
+ holder: owner, adapter: "conformance_boundary", commission: 10%, covers: purchase
20
21
  expose activate as activate_cover
21
22
  expose cancel as cancel_cover
22
23
  }
23
24
  attach claim = insurance.claim {
24
25
  expose create as create_claim
25
- cover: protection, inspector: inspector
26
+ cover: protection, inspector: operator
26
27
  expose approve as approve_claim
27
28
  expose deny as deny_claim
28
29
  expose pay as pay_claim
@@ -20,7 +20,7 @@ object business "Business funding" {
20
20
  attach loan = financing.installments {
21
21
  expose create as create_loan
22
22
  borrower: owner, capital: operator
23
- months: 12, profit: 6%, disburse_to: borrower
23
+ months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower
24
24
  limits: allowance, portfolio: budget
25
25
  expose sign as sign_loan
26
26
  expose disburse as release_loan
@@ -9,7 +9,7 @@ object car "Cars" {
9
9
  attach ceiling = financing.portfolio_limit { expose create as create_ceiling, limit: 1500000 SAR }
10
10
  attach plan = financing.installments {
11
11
  borrower: actor, capital: operator, share: 25%
12
- months: 3, profit: 2.5%, funds: sale, limits: limits, portfolio: ceiling
12
+ months: 3, pricing: flat_total, profit_rate: 2.5%, funds: sale, limits: limits, portfolio: ceiling
13
13
  expose create as finance
14
14
  }
15
15
  }
@@ -3,7 +3,7 @@
3
3
  program equipment_rental "Equipment rental"
4
4
  party renter: business
5
5
 
6
- // Program-local instruments have fixed terms and parties, not tunable parameters.
6
+ // The renter is one declared business, so this instrument takes no parameters.
7
7
  instrument deposit {
8
8
  summary: "Hold a rental deposit, then return it less any fixed late fee."
9
9
  fields {
@@ -19,7 +19,7 @@ object purchase "Equipment loan" {
19
19
  attach loan = financing.installments {
20
20
  expose create as create_loan
21
21
  borrower: owner, capital: operator
22
- months: 12, profit: 6%, disburse_to: borrower
22
+ months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower
23
23
  limits: allowance, portfolio: budget
24
24
  expose sign as sign_purchase_loan
25
25
  expose disburse as fund_purchase
@@ -11,7 +11,7 @@ object car "Car" {
11
11
  attach ceiling = financing.portfolio_limit { expose create as create_ceiling, limit: 10000000 SAR }
12
12
  attach plan = financing.installments {
13
13
  borrower: actor, capital: operator
14
- months: 36, profit: 8.5%, down_payment: 10%
14
+ months: 36, pricing: flat_total, profit_rate: 8.5%, down_payment: 10%
15
15
  apply: fines_profit_principal, payoff_rebate: 100%, write_off_after: 90d
16
16
  funds: sale, limits: limits, portfolio: ceiling
17
17
  expose create as finance, expose sign as sign_contract, expose payoff as payoff
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperscale0/hsx",
3
- "version": "1.0.125",
3
+ "version": "1.0.130",
4
4
  "description": "The strictly typed HSX language: compile total financial programs into canonical UDL.",
5
5
  "keywords": [
6
6
  "hsx",
@@ -84,8 +84,8 @@
84
84
  "examples:bundle": "bun scripts/bundle-examples.ts"
85
85
  },
86
86
  "dependencies": {
87
- "@hyperscale0/adl": "1.0.125",
88
- "@hyperscale0/udl": "1.0.125",
87
+ "@hyperscale0/adl": "1.0.130",
88
+ "@hyperscale0/udl": "1.0.130",
89
89
  "fast-sha256": "1.3.0"
90
90
  },
91
91
  "devDependencies": {
@@ -97,5 +97,5 @@
97
97
  "Amir Ayub",
98
98
  "Sara AlBakaawi"
99
99
  ],
100
- "gitHead": "44fcac0bdfa4468cf88215020ab25a407848f68c"
100
+ "gitHead": "3d768e3b8dd85648cb01257a02fef3b1e0b1ff57"
101
101
  }
package/src/cli.ts CHANGED
@@ -87,7 +87,7 @@ export async function runCli(argv: readonly string[], io: Io): Promise<number> {
87
87
  const result = compile(source, { standardLibrary });
88
88
  for (const d of result.diagnostics)
89
89
  io.err(
90
- `${d.source && d.source !== "program" ? d.source : file}:${d.line}:${d.column}: ${d.code} ${d.message}. ${d.fix}`,
90
+ `${d.source && d.source !== "program" ? d.source : file}:${d.line}:${d.column}: ${d.code} ${d.message.replace(/[^.?!]$/, "$&.")} ${d.fix}`,
91
91
  );
92
92
  if (!result.artifacts || result.verdict !== "valid") return 1;
93
93
  if (command === "check") return 0;
package/src/compile.ts CHANGED
@@ -534,14 +534,6 @@ export function compile(
534
534
  }
535
535
  for (const decl of program.decls)
536
536
  if (decl.kind === "instrument") templates.set(decl.name, decl);
537
- for (const decl of program.decls) {
538
- if (decl.kind === "instrument" && decl.parameters.length)
539
- fail(
540
- decl,
541
- "This compiler cannot instantiate a parameterized instrument declared inside a program.",
542
- "For this version, specialize the instrument with fixed bindings and remove its parameters. Custom reusable headers require a host-supplied library.",
543
- );
544
- }
545
537
  const document: UdlDocument = {
546
538
  udl: UDL_FORMAT_VERSION,
547
539
  version: 1,
@@ -906,8 +898,12 @@ export function compile(
906
898
  const typeName =
907
899
  type.kind === "type" || type.kind === "call" ? type.name : text(type);
908
900
  const partyParameter = attachmentInfo && typeName === "party";
901
+ const optional = type.kind === "type" && type.optional;
902
+ // An optional party stays unset unless the attachment binds it, so a
903
+ // program party that shares its name does not switch it on.
909
904
  const byName: Expr | undefined =
910
905
  partyParameter &&
906
+ !optional &&
911
907
  (subjectPartyRoles.includes(param.key as SubjectPartyRole) ||
912
908
  document.parties[param.key])
913
909
  ? { kind: "name", value: param.key, span: origin }
@@ -920,7 +916,7 @@ export function compile(
920
916
  source: declarationSources.get(decl) ?? "program",
921
917
  });
922
918
  if (!actual) {
923
- if (type.kind === "type" && type.optional) continue;
919
+ if (optional) continue;
924
920
  parameterErrors.push({
925
921
  span: origin,
926
922
  code: partyParameter ? "subject_party_unbound" : "HSX1001",
@@ -989,7 +985,12 @@ export function compile(
989
985
  .filter((entry) => entry.key.startsWith("action "))
990
986
  .flatMap(
991
987
  (entry) =>
992
- asBlock(entries(asBlock(entry.value)).get("subject")).entries,
988
+ // Read subject rows directly: entries() refuses repeated when branches.
989
+ asBlock(
990
+ asBlock(entry.value).entries.find(
991
+ (row) => row.key === "subject",
992
+ )?.value,
993
+ ).entries,
993
994
  );
994
995
  const moneyFields = [
995
996
  ...new Set(
@@ -1038,6 +1039,29 @@ export function compile(
1038
1039
  (!!attachmentInfo &&
1039
1040
  subjectPartyRoles.includes(name as SubjectPartyRole));
1040
1041
  const resolvedParties = new Set<string>();
1042
+ // A party of a sibling attachment this one references, such as the
1043
+ // borrower of `self.plan`, binds here the way it binds there.
1044
+ const reachedParty = (
1045
+ name: string,
1046
+ ): AttachmentPartyBinding | undefined => {
1047
+ if (!attachmentInfo) return;
1048
+ const targets = fields.flatMap((field) =>
1049
+ field.type === "ref" && field.targetKind === "instrument"
1050
+ ? [field.target].flat()
1051
+ : [],
1052
+ );
1053
+ const bindings = attachmentInfo.attachments.flatMap((attachment) =>
1054
+ targets.some(
1055
+ (target) =>
1056
+ target === attachment.instrument ||
1057
+ target.startsWith(`${attachment.instrument}_`),
1058
+ ) && attachment.parties[name]
1059
+ ? [attachment.parties[name]]
1060
+ : [],
1061
+ );
1062
+ const distinct = new Set(bindings.map((item) => JSON.stringify(item)));
1063
+ return distinct.size === 1 ? bindings[0] : undefined;
1064
+ };
1041
1065
  const resolve = (
1042
1066
  expr: Expr,
1043
1067
  seen = new Set<string>(),
@@ -1097,6 +1121,18 @@ export function compile(
1097
1121
  attachmentSubjects.has(name)
1098
1122
  ? name.slice(attachmentInfo.subjectKindId.length + 1)
1099
1123
  : `${name} (program)`;
1124
+ const elsewhere = [...assignments.values()]
1125
+ .filter(
1126
+ (assignment) =>
1127
+ assignment.target === type &&
1128
+ attachmentSubjects.has(assignment.name) &&
1129
+ attachmentSubjects.get(assignment.name) !==
1130
+ attachmentInfo.subjectKindId,
1131
+ )
1132
+ .map((assignment) => {
1133
+ const subject = attachmentSubjects.get(assignment.name)!;
1134
+ return `\`${subject}.${assignment.name.slice(subject.length + 1)}\``;
1135
+ });
1100
1136
  const target = templates.get(type);
1101
1137
  const required =
1102
1138
  target?.parameters
@@ -1115,7 +1151,7 @@ export function compile(
1115
1151
  }`,
1116
1152
  matches.length
1117
1153
  ? `Bind \`${parameter}\` explicitly to one of: ${matches.map(local).join(", ")}.`
1118
- : `Declare a ${parameter} attachment and bind \`${parameter}: allowance\`. Choose ${required.map((name) => (name.startsWith("per_") ? `its ${name.slice(4).replaceAll("_", " ")} limit` : `\`${name}\``)).join(", ") || "its required bindings"} explicitly.`,
1154
+ : `Attach \`${type}\` to \`${attachmentInfo.subjectKindId}\` and bind \`${parameter}\` to that attachment's name. Choose ${required.map((name) => (name.startsWith("per_") ? `its ${name.slice(4).replaceAll("_", " ")} limit` : `\`${name}\``)).join(", ") || "its required bindings"} explicitly.${elsewhere.length ? ` ${elsewhere.join(", ")} ${elsewhere.length === 1 ? "is" : "are"} on another object, which this binding cannot reach.` : ""}`,
1119
1155
  );
1120
1156
  }
1121
1157
  if (expr.name !== "all" && matches.length !== 1)
@@ -1554,6 +1590,28 @@ export function compile(
1554
1590
  value: `${id}_${key}`,
1555
1591
  span: origin,
1556
1592
  });
1593
+ // A subject role only has money here when this attachment binds it.
1594
+ const moneyPath = (expr: Expr): string => {
1595
+ const lowered = path(expr);
1596
+ const role = lowered.slice("party.".length) as SubjectPartyRole;
1597
+ if (
1598
+ attachmentInfo &&
1599
+ lowered.startsWith("party.") &&
1600
+ subjectPartyRoles.includes(role) &&
1601
+ !Object.values(attachmentInfo.parties).some(
1602
+ (binding) => "role" in binding && binding.role === role,
1603
+ )
1604
+ )
1605
+ failWithCode(
1606
+ expr,
1607
+ "subject_party_unbound",
1608
+ `\`${role}\` has no money account in \`${decl.name}\``,
1609
+ role === "operator"
1610
+ ? "Use `programOperator` for the company's own account."
1611
+ : `Add a party parameter to \`${decl.name}\` and bind it to \`${role}\` where you attach it.`,
1612
+ );
1613
+ return lowered;
1614
+ };
1557
1615
  const fields: UdlField[] = [];
1558
1616
  const calculations: UdlCalculation[] = [];
1559
1617
  let currentAction: UdlAction | undefined;
@@ -2286,12 +2344,12 @@ export function compile(
2286
2344
  };
2287
2345
  }
2288
2346
  }
2289
- const fromBinding = slots.get("from");
2290
- if (
2291
- fromBinding?.kind === "name" &&
2292
- fromBinding.value.includes(".") &&
2293
- slots.has("moves")
2294
- ) {
2347
+ const from = slots.get("from");
2348
+ // A policy member may sit in a state list such as `[funded, dispute.refund_after]`.
2349
+ const fromBinding = (from?.kind === "list" ? from.items : [from]).find(
2350
+ (item) => item?.kind === "name" && item.value.includes("."),
2351
+ );
2352
+ if (fromBinding?.kind === "name" && slots.has("moves")) {
2295
2353
  const [parameter, member] = fromBinding.value.split(".");
2296
2354
  const suppliedPolicy = supplied.get(parameter!);
2297
2355
  if (suppliedPolicy?.kind === "block") {
@@ -2332,6 +2390,7 @@ export function compile(
2332
2390
  ...(actionSubject ? { subject: actionSubject } : {}),
2333
2391
  };
2334
2392
  authored.set(a, row.value);
2393
+ const actionFeeCalculations: UdlCalculation[] = [];
2335
2394
  for (const binding of boundaryBindings)
2336
2395
  if (
2337
2396
  !a.subject?.adapters.find((entry) => entry.binding === binding)
@@ -2470,9 +2529,18 @@ export function compile(
2470
2529
  `unknown economic purpose ${purposeName}`,
2471
2530
  "choose a declared economic purpose",
2472
2531
  );
2473
- const sourcePartyName = sourceParty
2532
+ let sourcePartyName = sourceParty
2474
2533
  ? String(data(sourceParty))
2475
2534
  : undefined;
2535
+ const reached =
2536
+ sourcePartyName && !isParty(sourcePartyName)
2537
+ ? reachedParty(sourcePartyName)
2538
+ : undefined;
2539
+ if (reached) {
2540
+ attachmentInfo!.parties[sourcePartyName!] = reached;
2541
+ sourcePartyName =
2542
+ "role" in reached ? reached.role : reached.party;
2543
+ }
2476
2544
  if (sourcePartyName && !isParty(sourcePartyName))
2477
2545
  fail(
2478
2546
  sourceParty!,
@@ -2606,7 +2674,7 @@ export function compile(
2606
2674
  operation: "internal_transfer.create",
2607
2675
  ...(economics ? { economics } : {}),
2608
2676
  amount: piece,
2609
- from: path(from),
2677
+ from: moneyPath(from),
2610
2678
  to: `party.${recipient.value}`,
2611
2679
  });
2612
2680
  }
@@ -2634,8 +2702,8 @@ export function compile(
2634
2702
  ? { capture: String(data(parts.get("capture")!)) }
2635
2703
  : {}),
2636
2704
  amount: val(amount!),
2637
- from: path(from!),
2638
- to: path(to!),
2705
+ from: moneyPath(from!),
2706
+ to: moneyPath(to!),
2639
2707
  };
2640
2708
  const fee = parts.get("fee");
2641
2709
  if (!fee)
@@ -2692,6 +2760,11 @@ export function compile(
2692
2760
  "write a percentage such as 1%",
2693
2761
  );
2694
2762
  const prefix = `${name}_${key}`;
2763
+ // Instrument calculations run at create, before any action
2764
+ // input exists, so a fee on an input amount runs with its action.
2765
+ const perAction =
2766
+ "field" in transfer.amount &&
2767
+ transfer.amount.field.startsWith("input.");
2695
2768
  const derived = (
2696
2769
  suffix: string,
2697
2770
  label: string,
@@ -2704,8 +2777,13 @@ export function compile(
2704
2777
  `generated fee field ${target} conflicts with a field`,
2705
2778
  "rename the authored field or move key",
2706
2779
  );
2707
- fields.push({ name: target, type: "money", label });
2708
- calculations.push({
2780
+ fields.push({
2781
+ name: target,
2782
+ type: "money",
2783
+ label,
2784
+ ...(perAction ? { optional: true } : {}),
2785
+ });
2786
+ (perAction ? actionFeeCalculations : calculations).push({
2709
2787
  ...calculation,
2710
2788
  target,
2711
2789
  } as UdlCalculation);
@@ -2828,6 +2906,8 @@ export function compile(
2828
2906
  }
2829
2907
  } else (a as unknown as Record<string, unknown>)[key] = data(expr);
2830
2908
  }
2909
+ if (actionFeeCalculations.length)
2910
+ a.calculate = [...(a.calculate ?? []), ...actionFeeCalculations];
2831
2911
  if (attachmentInfo && !attachmentInfo.child) {
2832
2912
  if (attachmentInfo.exposed.has(name)) {
2833
2913
  if (automatic(a.actor))
@@ -2895,6 +2975,26 @@ export function compile(
2895
2975
  message: `${id}.${name}.subject.${requirement.field.name}`,
2896
2976
  });
2897
2977
  }
2978
+ // A parent actor over an empty all() names no instrument that could
2979
+ // invoke the action. A std action then does not exist in this program;
2980
+ // in user code the all() is a mistake.
2981
+ if (
2982
+ typeof a.actor === "object" &&
2983
+ "parent" in a.actor &&
2984
+ Array.isArray(a.actor.parent) &&
2985
+ !a.actor.parent.length
2986
+ ) {
2987
+ if ((declarationSources.get(decl) ?? "program") === "program") {
2988
+ const parent = entries(asBlock(slots.get("actor"))).get("parent");
2989
+ fail(
2990
+ parent ?? row,
2991
+ `${parent?.kind === "call" ? `${parent.name}(${text(parent.args[0]!)})` : `the parent actor of ${name}`} matches no instrument`,
2992
+ "name an instrument this program declares or attaches",
2993
+ );
2994
+ }
2995
+ delete lifecycle.transitions[name];
2996
+ continue;
2997
+ }
2898
2998
  inst.actions[name] = a;
2899
2999
  inst.actionOrder.push(name);
2900
3000
  }
@@ -3307,7 +3407,9 @@ export function compile(
3307
3407
  });
3308
3408
  };
3309
3409
  for (const decl of program.decls) {
3310
- if (decl.kind === "instrument") {
3410
+ // A parameterized program instrument is a template, like a header
3411
+ // instrument: each attachment instantiates it with its own bindings.
3412
+ if (decl.kind === "instrument" && !decl.parameters.length) {
3311
3413
  addInstrument(decl, decl.name, emptyBlock, decl.span);
3312
3414
  }
3313
3415
  if (decl.kind === "assignment") {
@@ -3647,7 +3749,9 @@ export function compile(
3647
3749
  fail(
3648
3750
  at,
3649
3751
  `\`${name}\` is not a declared party.${didYouMean(name, names)}`,
3650
- `Declare it with \`party ${name}: business\`, or name a declared party (${names.join(", ")}).`,
3752
+ subjectPartyRoles.includes(name as SubjectPartyRole)
3753
+ ? `Declare a party parameter such as \`holder: party\` on the instrument, use it here, and bind \`holder: ${name}\` where you attach it.`
3754
+ : `Declare it with \`party ${name}: business\`, or name a declared party (${names.join(", ")}).`,
3651
3755
  );
3652
3756
  };
3653
3757
  const checkInput = (
@@ -3725,13 +3829,100 @@ export function compile(
3725
3829
  inst,
3726
3830
  label: declarations.get(inst)!.name,
3727
3831
  });
3832
+ // A family member drops an optional reference field its binding left
3833
+ // unset (a borrower-funded plan has no `funds`), so it cannot match and
3834
+ // leaves the selection. The header must still declare the field.
3835
+ const declaredMembers = (selection: UdlSelection) => {
3836
+ const members = [selection.instrument].flat();
3837
+ const header = selection.family
3838
+ ? templates.get(
3839
+ `${selection.family.module}.${selection.family.exportPath}`,
3840
+ )
3841
+ : undefined;
3842
+ if (
3843
+ !header ||
3844
+ !entries(asBlock(entries(header.body).get("fields"))).has(
3845
+ selection.reference,
3846
+ )
3847
+ )
3848
+ return members;
3849
+ return members.filter((id) =>
3850
+ resolveField(document, byId.get(id)!, `self.${selection.reference}`),
3851
+ );
3852
+ };
3853
+ // A member whose reference points at another attachment of the anchor's
3854
+ // own template (the plan funding another object's escrow) cannot match
3855
+ // either, so each object only selects what can reference it.
3856
+ const kin = (a: string, b: string) => {
3857
+ const left = byId.get(a);
3858
+ const right = byId.get(b);
3859
+ return (
3860
+ !!left && !!right && declarations.get(left) === declarations.get(right)
3861
+ );
3862
+ };
3863
+ const matchable = (
3864
+ selection: UdlSelection,
3865
+ inst: UdlInstrument,
3866
+ action?: UdlAction,
3867
+ ) =>
3868
+ declaredMembers(selection).filter((id) => {
3869
+ const member = byId.get(id);
3870
+ if (!member) return true;
3871
+ const anchor = resolveField(
3872
+ document,
3873
+ inst,
3874
+ selection.anchor,
3875
+ action?.input,
3876
+ action,
3877
+ );
3878
+ const reference = resolveField(
3879
+ document,
3880
+ member,
3881
+ `self.${selection.reference}`,
3882
+ );
3883
+ if (anchor?.type !== "ref" || reference?.type !== "ref") return true;
3884
+ const targets = [anchor.target].flat();
3885
+ const candidates = [reference.target].flat();
3886
+ return !candidates.every(
3887
+ (candidate) =>
3888
+ !targets.includes(candidate) &&
3889
+ targets.some((target) => kin(target, candidate)),
3890
+ );
3891
+ });
3892
+ // A count over no matchable member is zero.
3893
+ for (const inst of document.instruments)
3894
+ for (const action of Object.values(inst.actions))
3895
+ action.requires = action.requires.map((requirement) =>
3896
+ requirement.kind === "aggregate" &&
3897
+ requirement.measure === "count" &&
3898
+ !matchable(requirement.selection, inst, action).length
3899
+ ? {
3900
+ kind: "compare",
3901
+ left: { literal: 0 },
3902
+ operator: requirement.operator,
3903
+ right: requirement.value,
3904
+ }
3905
+ : requirement,
3906
+ );
3728
3907
  const checkSelection = (
3729
3908
  selection: UdlSelection,
3730
3909
  at: Located,
3731
3910
  scope: Scope,
3732
3911
  ) => {
3912
+ walk(
3913
+ selection.overlaps,
3914
+ "overlaps",
3915
+ site(selection, "overlaps", at),
3916
+ scope,
3917
+ );
3733
3918
  walk(selection.anchor, "anchor", site(selection, "anchor", at), scope);
3734
3919
  walk(selection.where, "where", site(selection, "where", at), scope);
3920
+ const members = matchable(selection, scope.inst, scope.action);
3921
+ if (
3922
+ members.length &&
3923
+ members.length < [selection.instrument].flat().length
3924
+ )
3925
+ selection.instrument = members;
3735
3926
  for (const id of [selection.instrument].flat()) {
3736
3927
  checkTarget(id, site(selection, "instrument", at));
3737
3928
  const target = scopeOf(byId.get(id)!);
@@ -3747,6 +3938,9 @@ export function compile(
3747
3938
  selection.order?.forEach((key, index) =>
3748
3939
  member(key, site(selection.order!, index, at)),
3749
3940
  );
3941
+ if (selection.overlaps)
3942
+ for (const key of ["start", "end"] as const)
3943
+ member(selection.overlaps[key], site(selection.overlaps, key, at));
3750
3944
  if (selection.window)
3751
3945
  member(selection.window.field, site(selection.window, "field", at));
3752
3946
  }
@@ -4052,13 +4246,15 @@ export function compile(
4052
4246
  ),
4053
4247
  ),
4054
4248
  );
4249
+ const reaching = stranded.actions.at(-1);
4250
+ const drain = `move ${stranded.accounts.map((account) => `\`self.${account}.balance\``).join(" and ")} out in ${reaching ? `\`${reaching}\`` : "the action that reaches it"}, or add an action from \`${stranded.state}\` that does`;
4055
4251
  return diagnostic(
4056
4252
  {
4057
4253
  code: i.code,
4058
4254
  message: `\`${attachment?.name ?? instrument.id}\` can reach \`${stranded.state}\` with money in ${stranded.accounts.map((account) => `\`${account}\``).join(", ")}, but no action leaves that state and disposes of the balance.`,
4059
4255
  fix: binding
4060
- ? `Restore \`${binding.parameter}: ${stranded.state}\`, or author a complete refund path for every reachable funded state.`
4061
- : "Author a complete refund path for every reachable funded state.",
4256
+ ? `Restore \`${binding.parameter}: ${stranded.state}\`, or ${drain}.`
4257
+ : `${drain[0]!.toUpperCase()}${drain.slice(1)}.`,
4062
4258
  span: binding?.span ?? origin?.span ?? program.span,
4063
4259
  related: stranded.accounts.map((account) => ({
4064
4260
  source: "program",
@@ -1,19 +1,19 @@
1
1
  // Generated by open/hsx/scripts/bundle-examples.ts. Do not edit by hand.
2
2
  import type { HsxExample } from "./examples.ts";
3
3
  export const examples: HsxExample[] = [
4
- {"id":"cards","title":"Team expense cards","summary":"A company issues expense cards to employees and reserves each supplier payment before capture.","headers":["cards"],"source":"// A company issues expense cards to employees and reserves each supplier payment before capture.\nprogram team_expense_cards \"Team expense cards\"\nuse cards\nparty supplier: business\n\nobject employee \"Employee card\" {\n fields { employeeName: text, department: text }\n columns: [employeeName, department]\n attach holder = cards.cardholder {\n expose create as create_holder\n holder: owner\n expose activate as activate_employee\n expose suspend as suspend_employee\n }\n attach card = cards.card {\n expose create as create_card\n holder: holder, spend_limit: 5000 SAR\n expose activate as activate_card\n expose freeze as freeze_card\n expose unfreeze as unfreeze_card\n }\n attach payment = cards.authorization {\n expose create as create_payment\n card: card, merchant: supplier\n expose approve as approve_payment\n expose capture as capture_payment\n expose reverse as reverse_payment\n }\n attach receipt = cards.transaction {\n expose create as create_receipt\n authorization: payment\n expose refund as refund_payment\n }\n}\n"},
5
- {"id":"collections","title":"Small business collections","summary":"A business lender refers overdue instalments to an agency with limits on collection contacts.","headers":["collections","financing"],"source":"// A business lender refers overdue instalments to an agency with limits on collection contacts.\nprogram business_collections \"Small business collections\"\nuse collections\nuse financing\nparty agency: business\n\nobject facility \"Business loan\" {\n fields { businessName: text, purpose: text, price: money }\n columns: [businessName, purpose, price]\n attach allowance = financing.limits { expose create as create_allowance, expose approve as approve_borrower, borrower: owner, per_borrower: 100000 SAR }\n attach budget = financing.portfolio_limit { expose create as create_budget, expose approve as approve_budget, limit: 2000000 SAR }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, profit: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_loan\n expose disburse as release_loan\n expose payoff as repay_early\n }\n attach recovery = collections.case {\n expose create as create_recovery\n on: loan, agency: agency\n expose assign as assign_case\n expose recover as recover_payment\n expose recall as recall_case\n }\n attach contact = collections.contact {\n expose create as create_contact\n case: recovery, agency: agency\n }\n}\n\nexpose facility.loan.payment.create as create_payment\nexpose facility.loan.payment.pay as pay_instalment\nexpose facility.loan.payment.refund as refund_payment\nexpose facility.loan.payoff_quote.create as request_payoff_quote\n"},
4
+ {"id":"cards","title":"Team expense cards","summary":"A company issues expense cards to employees and reserves each supplier payment before capture.","headers":["cards"],"source":"// A company issues expense cards to employees and reserves each supplier payment before capture.\nprogram team_expense_cards \"Team expense cards\"\nuse cards\n// supplier is a business this program declares, so plan needs {\"partyBindings\":{\"supplier\":\"<partyId>\"}}.\n// That partyId is an organization customer's: create the Product from this source without the payment\n// and receipt attachments, add the customer with customer.create, activate its access, open its\n// customer_balance account, then compose this source with the binding.\nparty supplier: business\n\nobject employee \"Employee card\" {\n fields { employeeName: text, department: text }\n columns: [employeeName, department]\n attach holder = cards.cardholder {\n expose create as create_holder\n holder: owner\n expose activate as activate_employee\n expose suspend as suspend_employee\n }\n attach card = cards.card {\n expose create as create_card\n holder: holder, spend_limit: 5000 SAR\n expose activate as activate_card\n expose freeze as freeze_card\n expose unfreeze as unfreeze_card\n }\n attach payment = cards.authorization {\n expose create as create_payment\n card: card, merchant: supplier\n expose approve as approve_payment\n expose capture as capture_payment\n expose reverse as reverse_payment\n }\n attach receipt = cards.transaction {\n expose create as create_receipt\n authorization: payment\n expose refund as refund_payment\n }\n}\n"},
5
+ {"id":"collections","title":"Small business collections","summary":"A business lender refers overdue instalments to an agency with limits on collection contacts.","headers":["collections","financing"],"source":"// A business lender refers overdue instalments to an agency with limits on collection contacts.\nprogram business_collections \"Small business collections\"\nuse collections\nuse financing\n// agency is a business this program declares, so plan needs {\"partyBindings\":{\"agency\":\"<partyId>\"}}.\n// That partyId is an organization customer's: create the Product from this source without the recovery\n// and contact attachments, add the customer with customer.create, activate its access, open its\n// customer_balance account, then compose this source with the binding.\nparty agency: business\n\nobject facility \"Business loan\" {\n fields { businessName: text, purpose: text, price: money }\n columns: [businessName, purpose, price]\n attach allowance = financing.limits { expose create as create_allowance, expose approve as approve_borrower, borrower: owner, per_borrower: 100000 SAR }\n attach budget = financing.portfolio_limit { expose create as create_budget, expose approve as approve_budget, limit: 2000000 SAR }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_loan\n expose disburse as release_loan\n expose payoff as repay_early\n }\n attach recovery = collections.case {\n expose create as create_recovery\n on: loan, agency: agency\n expose assign as assign_case\n expose recover as recover_payment\n expose recall as recall_case\n }\n attach contact = collections.contact {\n expose create as create_contact\n case: recovery, agency: agency\n }\n}\n\nexpose facility.loan.payment.create as create_payment\nexpose facility.loan.payment.pay as pay_instalment\nexpose facility.loan.payment.refund as refund_payment\nexpose facility.loan.payoff_quote.create as request_payoff_quote\n"},
6
6
  {"id":"escrow","title":"Used device sales","summary":"Buyers fund a used device purchase, then accept delivery or raise a dispute before release.","headers":["escrow"],"source":"// Buyers fund a used device purchase, then accept delivery or raise a dispute before release.\nprogram used_device_sales \"Used device sales\"\nuse escrow\n\nobject device \"Device\" {\n fields {\n model: text\n condition: text\n serialNumber: text\n price: money\n }\n columns: [model, condition, price]\n attach sale = escrow.hold {\n expose create as create_sale\n payer: actor, payee: owner\n expose fund as pay_for_device\n expose deliver as confirm_delivery\n expose accept as accept_device\n expose dispute as report_problem\n expose verify_return as verify_return\n expose refund as refund_buyer\n expose cancel as cancel_sale\n }\n}\n"},
7
- {"id":"financing","title":"Tuition instalments","summary":"A tuition lender funds a student's fees over three interest-free instalments with borrowing limits.","headers":["financing"],"source":"// A tuition lender funds a student's fees over three interest-free instalments with borrowing limits.\nprogram tuition_instalments \"Tuition instalments\"\nuse financing\n\nobject enrolment \"Enrolment\" {\n fields { student: text, course: text, price: money }\n columns: [student, course, price]\n attach allowance = financing.limits {\n expose create as create_allowance\n borrower: owner, per_borrower: 12000 SAR\n expose approve as approve_student\n }\n attach budget = financing.portfolio_limit {\n expose create as create_budget\n limit: 500000 SAR\n expose approve as approve_budget\n }\n attach tuition = financing.installments {\n expose create as create_tuition\n borrower: owner, capital: operator\n months: 3, profit: 0%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_tuition_plan\n expose disburse as fund_tuition\n expose payoff as repay_early\n }\n}\n\nexpose enrolment.tuition.payment.create as create_payment\nexpose enrolment.tuition.payment.pay as pay_instalment\nexpose enrolment.tuition.payment.refund as refund_payment\nexpose enrolment.tuition.payoff_quote.create as request_payoff_quote\n"},
8
- {"id":"insurance","title":"Device insurance","summary":"A device retailer collects the purchase price and attaches cover with a claim review process.","headers":["insurance","money"],"source":"// A device retailer collects the purchase price and attaches cover with a claim review process.\nprogram device_insurance \"Device insurance\"\nuse insurance\nuse money\nparty inspector: staff role claims\n\nobject device \"Insured device\" {\n fields { model: text, serialNumber: text, purchaseDate: date }\n columns: [model, serialNumber, purchaseDate]\n attach purchase = money.transfer {\n expose create as create_purchase\n payer: owner, payee: operator\n economics pay { purpose: earning, sourceParty: owner }\n expose pay as pay_for_device\n }\n // Bind device_insurer to an ADL adapter before creating cover. Premiums net of commission go to its account.\n attach protection = insurance.cover {\n expose create as create_protection\n holder: owner, adapter: \"device_insurer\", commission: 10%, covers: purchase\n expose activate as activate_cover\n expose cancel as cancel_cover\n }\n attach claim = insurance.claim {\n expose create as create_claim\n cover: protection, inspector: inspector\n expose approve as approve_claim\n expose deny as deny_claim\n expose pay as pay_claim\n }\n}\n"},
9
- {"id":"lending","title":"Community business lending","summary":"A lending composition with limit approvals, wallet commitments and cash distribution transitions.","headers":["lending","financing","wallet"],"source":"// A lending composition with limit approvals, wallet commitments and cash distribution transitions.\nprogram community_business_lending \"Community business lending\"\nuse lending\nuse financing\nuse wallet\n\nobject business \"Business funding\" {\n fields { businessName: text, purpose: text, price: money }\n columns: [businessName, purpose, price]\n attach allowance = financing.limits {\n expose create as create_allowance\n borrower: owner, per_borrower: 100000 SAR\n expose approve as approve_borrower\n }\n attach budget = financing.portfolio_limit {\n expose create as create_budget\n limit: 2000000 SAR\n expose approve as approve_budget\n }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, profit: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_loan\n expose disburse as release_loan\n expose payoff as repay_early\n }\n attach investor_balance = wallet.balance {\n expose create as create_investor_balance\n holder: actor\n expose activate as open_wallet\n expose topup as add_funds\n }\n attach funding = lending.round {\n borrower: owner, plan: loan\n expose create as open_funding\n expose close as close_funding\n }\n attach investment = lending.commitment {\n round: funding, wallet: investor_balance, investor: actor\n expose create as invest\n expose withdraw as withdraw_investment\n }\n attach returns = lending.distribution {\n expose create as create_returns\n round: funding, receipt: loan.settlement\n expose prepare_cash as prepare_returns\n expose distribute_cash as distribute_returns\n }\n}\n\nexpose business.loan.payment.create as create_payment\nexpose business.loan.payment.pay as pay_instalment\nexpose business.loan.payment.refund as refund_payment\nexpose business.loan.payoff_quote.create as request_payoff_quote\nexpose business.returns.share.create as create_return_share\n"},
10
- {"id":"library","title":"Financed car sales","summary":"A minimal financed car sale connects escrow to a three-instalment plan and lending limits.","headers":["escrow","financing"],"source":"// A minimal financed car sale connects escrow to a three-instalment plan and lending limits.\nprogram cars \"Financed car sales\"\nuse escrow\nuse financing\nobject car \"Cars\" {\n fields { make: text, model: text, year: integer }\n attach sale = escrow.hold { expose create as create_sale, payer: actor, payee: owner, expose fund as sell }\n attach limits = financing.limits { expose create as create_limits, borrower: actor, per_borrower: 60000 SAR }\n attach ceiling = financing.portfolio_limit { expose create as create_ceiling, limit: 1500000 SAR }\n attach plan = financing.installments {\n borrower: actor, capital: operator, share: 25%\n months: 3, profit: 2.5%, funds: sale, limits: limits, portfolio: ceiling\n expose create as finance\n }\n}\n"},
7
+ {"id":"financing","title":"Tuition instalments","summary":"A tuition lender funds a student's fees over three interest-free instalments with borrowing limits.","headers":["financing"],"source":"// A tuition lender funds a student's fees over three interest-free instalments with borrowing limits.\nprogram tuition_instalments \"Tuition instalments\"\nuse financing\n\nobject enrolment \"Enrolment\" {\n fields { student: text, course: text, price: money }\n columns: [student, course, price]\n attach allowance = financing.limits {\n expose create as create_allowance\n borrower: owner, per_borrower: 12000 SAR\n expose approve as approve_student\n }\n attach budget = financing.portfolio_limit {\n expose create as create_budget\n limit: 500000 SAR\n expose approve as approve_budget\n }\n attach tuition = financing.installments {\n expose create as create_tuition\n borrower: owner, capital: operator\n months: 3, pricing: flat_total, profit_rate: 0%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_tuition_plan\n expose disburse as fund_tuition\n expose payoff as repay_early\n }\n}\n\nexpose enrolment.tuition.payment.create as create_payment\nexpose enrolment.tuition.payment.pay as pay_instalment\nexpose enrolment.tuition.payment.refund as refund_payment\nexpose enrolment.tuition.payoff_quote.create as request_payoff_quote\n"},
8
+ {"id":"insurance","title":"Device insurance","summary":"A device retailer collects the purchase price and attaches cover with a claim review process.","headers":["insurance","money"],"source":"// A device retailer collects the purchase price and attaches cover with a claim review process.\nprogram device_insurance \"Device insurance\"\nuse insurance\nuse money\n\nobject device \"Insured device\" {\n fields { model: text, serialNumber: text, purchaseDate: date }\n columns: [model, serialNumber, purchaseDate]\n attach purchase = money.transfer {\n expose create as create_purchase\n payer: owner, payee: operator\n economics pay { purpose: earning, sourceParty: owner }\n expose pay as pay_for_device\n }\n // conformance_boundary is the sandbox insurer adapter; swap in the insurer's registered adapter to go live.\n // Premiums net of commission go to its account. The default broker is the retailer, which keeps the commission.\n // claim_limit is the sum insured, set per cover at creation.\n attach protection = insurance.cover {\n expose create as create_protection\n holder: owner, adapter: \"conformance_boundary\", commission: 10%, covers: purchase\n expose activate as activate_cover\n expose cancel as cancel_cover\n }\n attach claim = insurance.claim {\n expose create as create_claim\n cover: protection, inspector: operator\n expose approve as approve_claim\n expose deny as deny_claim\n expose pay as pay_claim\n }\n}\n"},
9
+ {"id":"lending","title":"Community business lending","summary":"A lending composition with limit approvals, wallet commitments and cash distribution transitions.","headers":["lending","financing","wallet"],"source":"// A lending composition with limit approvals, wallet commitments and cash distribution transitions.\nprogram community_business_lending \"Community business lending\"\nuse lending\nuse financing\nuse wallet\n\nobject business \"Business funding\" {\n fields { businessName: text, purpose: text, price: money }\n columns: [businessName, purpose, price]\n attach allowance = financing.limits {\n expose create as create_allowance\n borrower: owner, per_borrower: 100000 SAR\n expose approve as approve_borrower\n }\n attach budget = financing.portfolio_limit {\n expose create as create_budget\n limit: 2000000 SAR\n expose approve as approve_budget\n }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_loan\n expose disburse as release_loan\n expose payoff as repay_early\n }\n attach investor_balance = wallet.balance {\n expose create as create_investor_balance\n holder: actor\n expose activate as open_wallet\n expose topup as add_funds\n }\n attach funding = lending.round {\n borrower: owner, plan: loan\n expose create as open_funding\n expose close as close_funding\n }\n attach investment = lending.commitment {\n round: funding, wallet: investor_balance, investor: actor\n expose create as invest\n expose withdraw as withdraw_investment\n }\n attach returns = lending.distribution {\n expose create as create_returns\n round: funding, receipt: loan.settlement\n expose prepare_cash as prepare_returns\n expose distribute_cash as distribute_returns\n }\n}\n\nexpose business.loan.payment.create as create_payment\nexpose business.loan.payment.pay as pay_instalment\nexpose business.loan.payment.refund as refund_payment\nexpose business.loan.payoff_quote.create as request_payoff_quote\nexpose business.returns.share.create as create_return_share\n"},
10
+ {"id":"library","title":"Financed car sales","summary":"A minimal financed car sale connects escrow to a three-instalment plan and lending limits.","headers":["escrow","financing"],"source":"// A minimal financed car sale connects escrow to a three-instalment plan and lending limits.\nprogram cars \"Financed car sales\"\nuse escrow\nuse financing\nobject car \"Cars\" {\n fields { make: text, model: text, year: integer }\n attach sale = escrow.hold { expose create as create_sale, payer: actor, payee: owner, expose fund as sell }\n attach limits = financing.limits { expose create as create_limits, borrower: actor, per_borrower: 60000 SAR }\n attach ceiling = financing.portfolio_limit { expose create as create_ceiling, limit: 1500000 SAR }\n attach plan = financing.installments {\n borrower: actor, capital: operator, share: 25%\n months: 3, pricing: flat_total, profit_rate: 2.5%, funds: sale, limits: limits, portfolio: ceiling\n expose create as finance\n }\n}\n"},
11
11
  {"id":"marketplace","title":"Freelance marketplace","summary":"Freelancers publish offers and clients place orders with funds held until work is accepted.","headers":["marketplace","escrow"],"source":"// Freelancers publish offers and clients place orders with funds held until work is accepted.\nprogram freelance_marketplace \"Freelance marketplace\"\nuse marketplace\nuse escrow\n\nobject project \"Project\" {\n fields { brief: text, deliverable: text, price: money }\n columns: [brief, deliverable, price]\n attach offer = marketplace.listing {\n expose create as create_offer\n seller: owner\n expose publish as publish_offer\n expose withdraw as withdraw_offer\n }\n attach order = marketplace.order {\n expose create as create_order\n listing: offer, buyer: actor\n expose commit as place_order\n expose fulfill as finish_order\n }\n attach payment = escrow.hold {\n expose create as create_payment\n payer: actor, payee: owner\n expose fund as fund_project\n expose deliver as submit_work\n expose accept as accept_work\n expose dispute as dispute_work\n expose verify_return as verify_return\n expose refund as refund_client\n expose cancel as cancel_payment\n }\n}\n"},
12
12
  {"id":"money","title":"Tutoring studio","summary":"A tutoring studio collects a fixed fee for each lesson and can cancel unpaid bookings.","headers":["money"],"source":"// A tutoring studio collects a fixed fee for each lesson and can cancel unpaid bookings.\nprogram tutoring_studio \"Tutoring studio\"\nuse money\n\nobject lesson \"Lesson\" {\n fields {\n student: text\n tutor: text\n startsAt: date\n }\n columns: [student, tutor, startsAt]\n attach payment = money.transfer {\n expose create as create_payment\n payer: actor, payee: operator, amount: 150 SAR\n economics pay { purpose: earning, sourceParty: actor }\n expose pay as pay_for_lesson\n expose cancel as cancel_payment\n }\n}\n"},
13
- {"id":"rental-deposit","title":"Equipment rental","summary":"A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee. On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.","headers":[],"source":"// A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee.\n// On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.\nprogram equipment_rental \"Equipment rental\"\nparty renter: business\n\n// Program-local instruments have fixed terms and parties, not tunable parameters.\ninstrument deposit {\n summary: \"Hold a rental deposit, then return it less any fixed late fee.\"\n fields {\n renter: account of renter\n held: account of self\n amount: money = 1000 SAR\n lateFee: money = 50 SAR\n dueAt: date\n refund: money?\n }\n lifecycle { states: [pending, funded, returned, cancelled], initial: pending }\n action create {\n actor: { party: renter }\n requires self.dueAt > self.now\n }\n action fund {\n from: pending, to: funded, actor: { party: renter }\n deadline: { at: self.dueAt }\n moves self.amount from self.renter to self.held\n }\n action return_on_time {\n from: funded, to: returned, actor: { party: programOperator }\n deadline: { at: self.dueAt }\n subject { returnReference: text }\n moves self.held.balance from self.held to self.renter\n }\n action return_late {\n from: funded, to: returned, actor: { party: programOperator }\n due: { at: self.dueAt }\n subject { returnReference: text }\n calculate: [{ target: refund, op: subtract, base: { field: self.held.balance }, subtract: [{ field: self.lateFee }] }]\n moves self.lateFee from self.held to programOperator\n moves self.refund from self.held to self.renter\n }\n action cancel { from: pending, to: cancelled, actor: { party: renter } }\n}\n\nobject rental \"Equipment rental\" {\n fields { equipment: text }\n columns: [equipment, dueAt]\n attach security = deposit {\n expose create as agree_rental\n expose fund as pay_deposit\n expose return_on_time as return_equipment\n expose return_late as return_equipment_late\n expose cancel as cancel_rental\n }\n}\n\nhide deposit.create\nhide deposit.fund\nhide deposit.return_on_time\nhide deposit.return_late\nhide deposit.cancel\n"},
13
+ {"id":"rental-deposit","title":"Equipment rental","summary":"A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee. On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.","headers":[],"source":"// A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee.\n// On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.\nprogram equipment_rental \"Equipment rental\"\nparty renter: business\n\n// The renter is one declared business, so this instrument takes no parameters.\ninstrument deposit {\n summary: \"Hold a rental deposit, then return it less any fixed late fee.\"\n fields {\n renter: account of renter\n held: account of self\n amount: money = 1000 SAR\n lateFee: money = 50 SAR\n dueAt: date\n refund: money?\n }\n lifecycle { states: [pending, funded, returned, cancelled], initial: pending }\n action create {\n actor: { party: renter }\n requires self.dueAt > self.now\n }\n action fund {\n from: pending, to: funded, actor: { party: renter }\n deadline: { at: self.dueAt }\n moves self.amount from self.renter to self.held\n }\n action return_on_time {\n from: funded, to: returned, actor: { party: programOperator }\n deadline: { at: self.dueAt }\n subject { returnReference: text }\n moves self.held.balance from self.held to self.renter\n }\n action return_late {\n from: funded, to: returned, actor: { party: programOperator }\n due: { at: self.dueAt }\n subject { returnReference: text }\n calculate: [{ target: refund, op: subtract, base: { field: self.held.balance }, subtract: [{ field: self.lateFee }] }]\n moves self.lateFee from self.held to programOperator\n moves self.refund from self.held to self.renter\n }\n action cancel { from: pending, to: cancelled, actor: { party: renter } }\n}\n\nobject rental \"Equipment rental\" {\n fields { equipment: text }\n columns: [equipment, dueAt]\n attach security = deposit {\n expose create as agree_rental\n expose fund as pay_deposit\n expose return_on_time as return_equipment\n expose return_late as return_equipment_late\n expose cancel as cancel_rental\n }\n}\n\nhide deposit.create\nhide deposit.fund\nhide deposit.return_on_time\nhide deposit.return_late\nhide deposit.cancel\n"},
14
14
  {"id":"repair-approval","title":"Repair approvals","summary":"A repair shop records inspection and approval evidence before it can complete a job.","headers":[],"source":"// A repair shop records inspection and approval evidence before it can complete a job.\nprogram repair_shop \"Repair approvals\"\n\ninstrument assessment {\n summary: \"Inspect the repair, collect approval, then record completion.\"\n fields { inspectedAt: date?, approvedAt: date? }\n lifecycle { states: [submitted, inspected, approved, completed, rejected], initial: submitted }\n action create {}\n action inspect {\n from: submitted, to: inspected, actor: { party: programOperator }\n subject { inspectionNotes: text }\n set: { inspectedAt: { field: self.now } }\n }\n action approve {\n from: inspected, to: approved, actor: { party: programOperator }\n subject { approvalReference: text }\n set: { approvedAt: { field: self.now } }\n }\n action reject {\n from: inspected, to: rejected, actor: { party: programOperator }\n }\n action complete {\n from: approved, to: completed, actor: { party: programOperator }\n subject { completionNotes: text }\n }\n}\n\nobject repair \"Repair\" {\n fields { item: text, problem: text }\n columns: [item, problem]\n attach review = assessment {\n expose create as create_review\n expose inspect as inspect_repair\n expose approve as approve_repair\n expose reject as reject_repair\n expose complete as complete_repair\n }\n}\n\nhide assessment.create\nhide assessment.inspect\nhide assessment.approve\nhide assessment.reject\nhide assessment.complete\n"},
15
- {"id":"reporting","title":"Equipment loan reports","summary":"An equipment lender keeps an ageing report over its financed purchases and recorded repayments.","headers":["reporting","financing"],"source":"// An equipment lender keeps an ageing report over its financed purchases and recorded repayments.\nprogram equipment_loan_reports \"Equipment loan reports\"\nuse reporting\nuse financing\n\nobject purchase \"Equipment loan\" {\n fields { businessName: text, equipment: text, price: money }\n columns: [businessName, equipment, price]\n attach allowance = financing.limits {\n expose create as create_allowance\n borrower: owner, per_borrower: 200000 SAR\n expose approve as approve_business\n }\n attach budget = financing.portfolio_limit {\n expose create as create_budget\n limit: 5000000 SAR\n expose approve as approve_budget\n }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, profit: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_purchase_loan\n expose disburse as fund_purchase\n }\n attach reports = reporting.portfolio { expose create as create_reports, on: loan }\n}\n"},
15
+ {"id":"reporting","title":"Equipment loan reports","summary":"An equipment lender keeps an ageing report over its financed purchases and recorded repayments.","headers":["reporting","financing"],"source":"// An equipment lender keeps an ageing report over its financed purchases and recorded repayments.\nprogram equipment_loan_reports \"Equipment loan reports\"\nuse reporting\nuse financing\n\nobject purchase \"Equipment loan\" {\n fields { businessName: text, equipment: text, price: money }\n columns: [businessName, equipment, price]\n attach allowance = financing.limits {\n expose create as create_allowance\n borrower: owner, per_borrower: 200000 SAR\n expose approve as approve_business\n }\n attach budget = financing.portfolio_limit {\n expose create as create_budget\n limit: 5000000 SAR\n expose approve as approve_budget\n }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_purchase_loan\n expose disburse as fund_purchase\n }\n attach reports = reporting.portfolio { expose create as create_reports, on: loan }\n}\n"},
16
16
  {"id":"savings","title":"Savings circle","summary":"Six members contribute to a shared pot and receive it in their agreed calendar order.","headers":["savings"],"source":"// Six members contribute to a shared pot and receive it in their agreed calendar order.\nprogram neighbourhood_savings \"Savings circle\"\nuse savings\n\nobject group \"Savings group\" {\n fields { groupName: text, purpose: text }\n columns: [groupName, purpose]\n attach circle = savings.circle {\n expose create as create_circle\n contribution: 500 SAR, members: 6, starts: 2027-01-01\n expose disband as disband_circle\n expose close as close_circle\n }\n attach membership = savings.membership {\n expose create as create_membership\n circle: circle, member: actor\n expose activate as confirm_membership\n expose withdraw as leave_circle\n }\n // Contributions and each member's payout follow the declared calendar.\n}\n"},
17
- {"id":"serviced","title":"Car financing","summary":"A car finance company services loans with late charges, payment reminders and early repayment.","headers":["escrow","financing","collections"],"source":"// A car finance company services loans with late charges, payment reminders and early repayment.\nprogram car_financing \"Car financing\"\nuse escrow\nuse financing\nuse collections\nobject car \"Car\" {\n fields { make: text, model: text, year: integer, vin: text, price: money }\n columns: [make, model, year, vin, price]\n attach sale = escrow.hold { expose create as create_sale, payer: actor, payee: operator, expose fund as purchase }\n attach limits = financing.limits { expose create as create_limits, borrower: actor, per_borrower: 250000 SAR }\n attach ceiling = financing.portfolio_limit { expose create as create_ceiling, limit: 10000000 SAR }\n attach plan = financing.installments {\n borrower: actor, capital: operator\n months: 36, profit: 8.5%, down_payment: 10%\n apply: fines_profit_principal, payoff_rebate: 100%, write_off_after: 90d\n funds: sale, limits: limits, portfolio: ceiling\n expose create as finance, expose sign as sign_contract, expose payoff as payoff\n }\n attach late = financing.late_charge {\n expose create as create_late\n on: plan, grace: 3d, fine: 50 SAR, cap: 25 SAR\n fines_to: operator, costs_to: operator, borrower: actor\n }\n attach reminders = collections.reminder { expose create as create_reminders, on: plan, operator: operator, before_days: 3, overdue_days: 1 }\n}\n"},
17
+ {"id":"serviced","title":"Car financing","summary":"A car finance company services loans with late charges, payment reminders and early repayment.","headers":["escrow","financing","collections"],"source":"// A car finance company services loans with late charges, payment reminders and early repayment.\nprogram car_financing \"Car financing\"\nuse escrow\nuse financing\nuse collections\nobject car \"Car\" {\n fields { make: text, model: text, year: integer, vin: text, price: money }\n columns: [make, model, year, vin, price]\n attach sale = escrow.hold { expose create as create_sale, payer: actor, payee: operator, expose fund as purchase }\n attach limits = financing.limits { expose create as create_limits, borrower: actor, per_borrower: 250000 SAR }\n attach ceiling = financing.portfolio_limit { expose create as create_ceiling, limit: 10000000 SAR }\n attach plan = financing.installments {\n borrower: actor, capital: operator\n months: 36, pricing: flat_total, profit_rate: 8.5%, down_payment: 10%\n apply: fines_profit_principal, payoff_rebate: 100%, write_off_after: 90d\n funds: sale, limits: limits, portfolio: ceiling\n expose create as finance, expose sign as sign_contract, expose payoff as payoff\n }\n attach late = financing.late_charge {\n expose create as create_late\n on: plan, grace: 3d, fine: 50 SAR, cap: 25 SAR\n fines_to: operator, costs_to: operator, borrower: actor\n }\n attach reminders = collections.reminder { expose create as create_reminders, on: plan, operator: operator, before_days: 3, overdue_days: 1 }\n}\n"},
18
18
  {"id":"wallet","title":"Workshop credits","summary":"Members prepay for workshop visits and the studio reserves each spend before charging it.","headers":["wallet"],"source":"// Members prepay for workshop visits and the studio reserves each spend before charging it.\nprogram workshop_credits \"Workshop credits\"\nuse wallet\n\nobject membership \"Membership\" {\n fields { memberName: text, membershipNumber: text }\n columns: [memberName, membershipNumber]\n attach balance = wallet.balance {\n expose create as create_balance\n holder: owner\n expose activate as open_balance\n expose topup as add_credit\n expose withdraw as withdraw_credit\n expose close as close_balance\n }\n attach visit = wallet.spend {\n expose create as create_visit\n wallet: balance, payee: operator\n economics pay { purpose: earning, sourceParty: owner }\n expose reserve as reserve_visit\n expose pay as charge_visit\n expose cancel as cancel_visit\n }\n}\n"},
19
19
  ];
package/src/headers.ts CHANGED
@@ -11,6 +11,7 @@ export const HEADER_NAMES = [
11
11
  "money",
12
12
  "marketplace",
13
13
  "escrow",
14
+ "booking",
14
15
  "wallet",
15
16
  "financing",
16
17
  "lending",