@hyperscale0/hsx 1.0.258 → 1.0.284

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 (47) hide show
  1. package/CHANGELOG.md +39 -4
  2. package/README.md +33 -4
  3. package/SECURITY.md +10 -21
  4. package/bases/cards.hsx +38 -0
  5. package/bases/collections.hsx +51 -0
  6. package/bases/escrow.hsx +23 -0
  7. package/bases/financing.hsx +36 -0
  8. package/bases/insurance.hsx +22 -0
  9. package/bases/lending.hsx +73 -0
  10. package/bases/marketplace.hsx +38 -0
  11. package/bases/money.hsx +12 -0
  12. package/bases/reporting.hsx +40 -0
  13. package/bases/savings.hsx +20 -0
  14. package/bases/wallet.hsx +23 -0
  15. package/dist/src/bases-bundle.d.ts +3 -0
  16. package/dist/src/bases-bundle.d.ts.map +1 -0
  17. package/dist/src/bases-bundle.js +14 -0
  18. package/dist/src/bases-bundle.js.map +1 -0
  19. package/dist/src/bases.d.ts +10 -0
  20. package/dist/src/bases.d.ts.map +1 -0
  21. package/dist/src/bases.js +2 -0
  22. package/dist/src/bases.js.map +1 -0
  23. package/dist/src/compile.d.ts.map +1 -1
  24. package/dist/src/compile.js +108 -14
  25. package/dist/src/compile.js.map +1 -1
  26. package/dist/src/examples-bundle.js +3 -3
  27. package/dist/src/examples-bundle.js.map +1 -1
  28. package/dist/src/std-bundle.d.ts.map +1 -1
  29. package/dist/src/std-bundle.js +7 -6
  30. package/dist/src/std-bundle.js.map +1 -1
  31. package/docs/README.md +40 -31
  32. package/examples/library.hsx +7 -1
  33. package/examples/rental-deposit.hsx +5 -12
  34. package/examples/serviced.hsx +7 -1
  35. package/package.json +13 -7
  36. package/src/bases-bundle.ts +15 -0
  37. package/src/bases.ts +9 -0
  38. package/src/compile.ts +138 -19
  39. package/src/examples-bundle.ts +3 -3
  40. package/src/std-bundle.ts +7 -6
  41. package/std/booking.hsx +8 -3
  42. package/std/collections.hsx +1 -1
  43. package/std/escrow.hsx +16 -29
  44. package/std/financing.hsx +21 -48
  45. package/std/money.hsx +6 -8
  46. package/std/purchase.hsx +89 -0
  47. package/std/reporting.hsx +1 -11
@@ -1 +1 @@
1
- {"version":3,"file":"std-bundle.js","sourceRoot":"","sources":["../../src/std-bundle.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,MAAM,CAAC,MAAM,iBAAiB,GAAgC,IAAI,GAAG,CAAiB;IACpF,CAAC,aAAa,EAAE,soLAAsoL,CAAC;IACvpL,CAAC,WAAW,EAAE,6wMAA6wM,CAAC;IAC5xM,CAAC,iBAAiB,EAAE,2sNAA2sN,CAAC;IAChuN,CAAC,YAAY,EAAE,yjQAAyjQ,CAAC;IACzkQ,CAAC,eAAe,EAAE,6nrFAA6nrF,CAAC;IAChprF,CAAC,eAAe,EAAE,41LAA41L,CAAC;IAC/2L,CAAC,aAAa,EAAE,03UAA03U,CAAC;IAC34U,CAAC,iBAAiB,EAAE,syTAAsyT,CAAC;IAC3zT,CAAC,WAAW,EAAE,8zYAA8zY,CAAC;IAC70Y,CAAC,eAAe,EAAE,ir2BAAir2B,CAAC;IACps2B,CAAC,aAAa,EAAE,4hKAA4hK,CAAC;IAC7iK,CAAC,YAAY,EAAE,2zIAA2zI,CAAC;CAC50I,CAAC,CAAC"}
1
+ {"version":3,"file":"std-bundle.js","sourceRoot":"","sources":["../../src/std-bundle.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,MAAM,CAAC,MAAM,iBAAiB,GAAgC,IAAI,GAAG,CAAiB;IACpF,CAAC,aAAa,EAAE,m7LAAm7L,CAAC;IACp8L,CAAC,WAAW,EAAE,6wMAA6wM,CAAC;IAC5xM,CAAC,iBAAiB,EAAE,qrNAAqrN,CAAC;IAC1sN,CAAC,YAAY,EAAE,mkOAAmkO,CAAC;IACnlO,CAAC,eAAe,EAAE,kinFAAkinF,CAAC;IACrjnF,CAAC,eAAe,EAAE,41LAA41L,CAAC;IAC/2L,CAAC,aAAa,EAAE,03UAA03U,CAAC;IAC34U,CAAC,iBAAiB,EAAE,syTAAsyT,CAAC;IAC3zT,CAAC,WAAW,EAAE,ogYAAogY,CAAC;IACnhY,CAAC,cAAc,EAAE,svJAAsvJ,CAAC;IACxwJ,CAAC,eAAe,EAAE,4o1BAA4o1B,CAAC;IAC/p1B,CAAC,aAAa,EAAE,4hKAA4hK,CAAC;IAC7iK,CAAC,YAAY,EAAE,2zIAA2zI,CAAC;CAC50I,CAAC,CAAC"}
package/docs/README.md CHANGED
@@ -42,10 +42,10 @@ hsx cost lesson.hsx
42
42
  `check` exits zero without output on success. `build` writes canonical UDL;
43
43
  without `--out` it prints to stdout. `cost` prints instruction counts, described
44
44
  below. `format` prints formatted source or writes it with `--out`. `headers --json`
45
- prints the header manifest. These commands have no `--strict` or `--catalog` option.
46
- Exit 1 means invalid source; exit 2 means a command or file error. Diagnostics name
47
- the source, line, column, code, problem and suggested fix. UDL validation keeps
48
- its UDL diagnostic codes; binding errors can have names such as
45
+ prints the header manifest. Exit 1 means invalid source; exit 2 means a command
46
+ or file error. Diagnostics name the source, line, column, code, problem and
47
+ suggested fix. UDL validation keeps its UDL diagnostic codes; binding errors can
48
+ have names such as
49
49
  `subject_party_unbound`. Read the message and fix, not just the code.
50
50
 
51
51
  At runtime, create a lesson object first, then call `book_lesson` to create its
@@ -57,38 +57,37 @@ and party IDs are not metadata supplied by the caller.
57
57
  ## A rental deposit in one hour
58
58
 
59
59
  Open [rental-deposit.hsx](../examples/rental-deposit.hsx). It is a complete source
60
- file for a business renting equipment from the program operator. Copy it to
60
+ file for a customer renting equipment from the program operator. Copy it to
61
61
  `rental.hsx` and run the same check, build and cost commands against that file.
62
62
  No header import is needed because it declares its own instrument.
63
63
 
64
- The terms are a 1,000 SAR deposit and a one-time 50 SAR late fee. The supplied
65
- `dueAt` timestamp is the cutoff: a return before it gets the whole deposit back;
66
- a return at or after it gets 950 SAR back. The operator receives the other 50 SAR.
67
- The late fee is not a daily charge and is not `financing.late_charge`, which
64
+ The terms are a 1,000 SAR deposit and one late charge of 50 SAR plus 15% VAT,
65
+ which is 7.50 SAR. The record's `startsAt` and `dueAt` timestamps reach the
66
+ agreement through `rename`, so `agree_rental` reads them from the object. A
67
+ return before `dueAt` gets the whole deposit back; a return at or after it gets
68
+ 942.50 SAR back. The operator receives 50 SAR and `programTax` receives 7.50 SAR.
69
+ The late charge is not a daily charge and is not `financing.late_charge`, which
68
70
  requires a financing plan and an overdue installment.
69
71
 
70
- | Step | Public action | Actor and required values | Money effect |
71
- | ---------------------------- | ----------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------------- |
72
- | Create the object | Host object creation | Optional `equipment` metadata | None |
73
- | Agree terms | `agree_rental` | Bound renter; `dueAt`, for example `2027-01-10T12:00:00+03:00`, must be in the future | Creates the agreement's held account |
74
- | Fund before the cutoff | `pay_deposit` | Bound renter with 1,000 SAR available | Renter pays 1,000 SAR into held |
75
- | Record an on-time return | `return_equipment` | Operator; `returnReference` metadata | Held pays 1,000 SAR to renter |
76
- | Or record a late return | `return_equipment_late` | Operator; `returnReference` metadata | Held pays 50 SAR to operator and 950 SAR to renter |
77
- | Cancel an unfunded agreement | `cancel_rental` | Bound renter, while pending | None |
72
+ | Step | Public action | Actor and required values | Money effect |
73
+ | ---------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
74
+ | Create the object | Host object creation | Optional `equipment`, `serialNumber`, `renterName`, `startsAt` and `dueAt` metadata | None |
75
+ | Agree terms | `agree_rental` | Bound renter; `startsAt` and `dueAt`, for example `2027-01-10T12:00:00+03:00`, both future, `dueAt` later | Creates the agreement's held account |
76
+ | Fund before pickup | `pay_deposit` | Bound renter with 1,000 SAR available, before `startsAt` | Renter pays 1,000 SAR into held |
77
+ | Record an on-time return | `return_equipment` | Operator, before `dueAt`; `returnReference` metadata | Held pays 1,000 SAR to renter |
78
+ | Or record a late return | `return_equipment_late` | Operator, at or after `dueAt`; `returnReference` metadata | Held pays 50 SAR to operator, 7.50 SAR to tax and 942.50 SAR to renter |
79
+ | Cancel before pickup | `cancel_before_pickup` | Bound renter on a funded agreement, before `startsAt` | Held pays 1,000 SAR back to renter |
80
+ | Cancel an unfunded agreement | `cancel_unpaid_rental` | Bound renter, while pending | None |
78
81
 
79
82
  Choose one return action. Both finish the agreement, so the fee and refund cannot
80
- repeat. The late action calculates the refund from the held balance before either
83
+ repeat. The late action calculates the refund from the held balance before any
81
84
  move runs. Both paths empty the held account. `returnReference` records the
82
85
  operator's return evidence; it does not verify the physical return. Time means
83
86
  the executor's current time when the action runs, not a caller-supplied return date.
84
87
  A late `due` permits the operator action; it does not schedule that action.
85
88
 
86
- This sample binds a declared business named `renter` in Product configuration.
87
- The renter is one business for the whole Build, so the instrument takes no
88
- parameters. Changing the renter binding is a Build configuration decision, not a
89
- different party ID on each request. For per-object participants, give a program
90
- instrument parameters as a header would, such as `instrument purse(holder: party)`,
91
- and bind `holder: owner` or `holder: actor` in each `attach` block. A
89
+ The renter is a party parameter bound to the rental object's `owner`. Each rental
90
+ can belong to a different customer without a fixed business binding. A
92
91
  parameterized program instrument exists only through its attachments. A `fee` on
93
92
  a move of an action input, such as `moves input.amount from holder to self.held
94
93
  fee { seller: 1% }`, is calculated when that action runs. Reusable custom headers
@@ -97,8 +96,7 @@ this CLI.
97
96
 
98
97
  Inspect `objects` in the compiled UDL for the attachment and public names, then
99
98
  its instrument for fields, transitions, actors, requirements and ordered moves.
100
- The `hide deposit.*` lines remove public names from the unattached declaration;
101
- the attachment exposes its own copy. `money.hold` and `escrow.hold` both refund
99
+ `money.hold` and `escrow.hold` both refund
102
100
  whole amounts, so neither implements this partial-refund policy by itself.
103
101
 
104
102
  A local compile gives you a contract. Executing it needs a host with authenticated
@@ -189,7 +187,7 @@ and basis points. Fee and tax calculations round down. Split percentages sum to
189
187
  schedules use positions 1 through n and put the division remainder in position 1.
190
188
  Amortizing financing is the exception: it rounds half up and puts the remainder in
191
189
  the last instalment, as a standard amortization table does.
192
- Tax binds `programTax`. Fees default to `programOperator`. Fine, recovery and
190
+ Tax binds `programTax`. Fees go to `programOperator`. Fine, recovery and
193
191
  residual destinations are party tunables.
194
192
 
195
193
  Attached actions are private unless the attachment exposes them with
@@ -284,8 +282,9 @@ maxAge 1d` requires a recent completed provider check.
284
282
  only to reservations. Fees settle with create moves, never reservations. A fee move
285
283
  also derives `<action>_<move>_debit`, the payer's whole debit: amount plus fee and
286
284
  tax when the buyer pays, the amount alone when the seller pays. Repeated clauses keep
287
- their declaration order. The JSON-like clause form remains accepted and lowers
288
- to the same [UDL clauses](https://github.com/hyperscale0/hyperscale-udl/blob/main/spec/README.md).
285
+ their declaration order. A clause list after a colon, such as `calculate: [{ ... }]`
286
+ or `invoke: [{ ... }]`, writes [UDL clauses](https://github.com/hyperscale0/hyperscale-udl/blob/main/spec/README.md)
287
+ directly; the std headers use it for calculations, `set` and invocations.
289
288
  Use `economics { purpose: earning, sourceParty: merchant }` on a move to
290
289
  classify posted money. Other purposes are `principal`, `participant_payout`,
291
290
  `internal`, `prepaid_credit`, and `pass_through`. `reversalOf: self.originalTransfer` links a
@@ -369,7 +368,8 @@ the selection limit, so a count is an upper bound rather than observed usage.
369
368
  Zero-valued moves can cost fewer actual transfers than their static count.
370
369
 
371
370
  The rental attachment's create declares one owned account, fund declares one
372
- transfer, on-time return one, and late return two. These are counts, not SAR
371
+ transfer, on-time return one, and late return three: the charge, its VAT and
372
+ the refund. These are counts, not SAR
373
373
  prices. The executor's tariff supplies commercial prices and actual usage.
374
374
 
375
375
  The compiler API is `compile(source)`. On success, `result.verdict` is `valid`
@@ -395,7 +395,16 @@ customer: `financing.limits` and `financing.installments` (borrower),
395
395
  (member). Several customers can each start one on the same record. A plan,
396
396
  commitment or other reference resolves the sibling that binds the same
397
397
  accounts to the parties both share, so each customer's plan finds its own
398
- escrow and each investor's commitment finds their own wallet.
398
+ checkout and each investor's commitment finds their own wallet.
399
+
400
+ A financed purchase connects `financing.installments` to `escrow.hold` through
401
+ `purchase.checkout`. Bind the plan's `funds` to checkout, checkout's `funds` to
402
+ the hold, and checkout's `plans` to the plan. Give the hold the funding policy
403
+ `{ controllers: [checkout], reference: "funds", blocking_states: [active] }`.
404
+ If a marketplace reservation contributes a deposit, its `converters` also names
405
+ checkout. Create the hold, then checkout, then the plan. The coordinator collects
406
+ the missing down payment and returns outstanding lender capital before a buyer
407
+ refund. See [library.hsx](../examples/library.hsx) for the complete wiring.
399
408
 
400
409
  `financing.limits` and `financing.portfolio_limit` declare `scope: product`.
401
410
  Create and approve the portfolio limit once per Product and the borrower limit
@@ -3,6 +3,7 @@
3
3
  program cars "Riyadh used van finance"
4
4
  use escrow
5
5
  use financing
6
+ use purchase
6
7
  party dealer: business
7
8
 
8
9
  // Bind dealer to the vehicle seller. Set price to its gross 34,500 SAR invoice;
@@ -13,6 +14,7 @@ object car "Financed van purchase" {
13
14
  columns: [make, model, year, vin, price]
14
15
  attach sale = escrow.hold {
15
16
  payer: actor, payee: dealer
17
+ funding: { controllers: [checkout], reference: "funds", blocking_states: [active] }
16
18
  fee: { seller: 2%, tax: 15% }
17
19
  deliver_within: 7d, accept_within: 3d
18
20
  expose create as agree_van_purchase
@@ -42,10 +44,14 @@ object car "Financed van purchase" {
42
44
  // At 34,500 SAR, 2.5% flat profit is 862.50 SAR for the whole term.
43
45
  // Financing fee: 0%, tax: 0% on principal; no separate financing service fee.
44
46
  // Supply three future monthly dates and a unique settlementIdentity.
47
+ attach checkout = purchase.checkout {
48
+ plans: plan, funds: sale, borrower: actor, capital: operator
49
+ expose create as prepare_financed_purchase
50
+ }
45
51
  attach plan = financing.installments {
46
52
  borrower: actor, capital: operator, share: 0%
47
53
  months: 3, pricing: flat_total, profit_rate: 2.5%
48
- funds: sale, limits: limits, portfolio: ceiling
54
+ funds: checkout, limits: limits, portfolio: ceiling
49
55
  returned_goods: refund_all, refund_within: 30d, write_off_after: 90d
50
56
  expose create as finance
51
57
  expose sign as sign_finance_agreement
@@ -1,11 +1,10 @@
1
- // Dammam equipment hire holds a 1,000 SAR deposit for a business renter.
1
+ // Equipment rental holds a 1,000 SAR deposit for the customer who owns each rental.
2
2
  // Return the deposit in full, or deduct a single 50 SAR late charge plus 7.50 SAR VAT.
3
- program equipment_rental "Dammam equipment hire"
4
- party renter: business
3
+ program equipment_rental "Equipment rental deposits"
5
4
 
6
- // Bind renter to the business hiring the equipment. The deposit has fee 0%
5
+ // Bind renter to the customer who owns this rental. The deposit has fee 0%
7
6
  // and tax 0% because it stays refundable principal. The late service has 15% VAT.
8
- instrument deposit {
7
+ instrument deposit(renter: party) {
9
8
  summary: "Hold security until return, with one disclosed late charge."
10
9
  fields {
11
10
  renter: account of renter
@@ -56,6 +55,7 @@ object rental "Equipment deposit" {
56
55
  fields { equipment: text, serialNumber: text, renterName: text }
57
56
  columns: [equipment, serialNumber, renterName, startsAt, dueAt]
58
57
  attach security = deposit {
58
+ renter: owner
59
59
  rename { startsAt: startsAt, dueAt: dueAt }
60
60
  expose create as agree_rental
61
61
  expose fund as pay_deposit
@@ -65,10 +65,3 @@ object rental "Equipment deposit" {
65
65
  expose cancel as cancel_unpaid_rental
66
66
  }
67
67
  }
68
-
69
- hide deposit.create
70
- hide deposit.fund
71
- hide deposit.return_on_time
72
- hide deposit.return_late
73
- hide deposit.refund_before_pickup
74
- hide deposit.cancel
@@ -3,6 +3,7 @@
3
3
  program car_financing "Dammam delivery van finance"
4
4
  use escrow
5
5
  use financing
6
+ use purchase
6
7
  use collections
7
8
  party dealer: business
8
9
 
@@ -13,6 +14,7 @@ object car "Delivery van finance" {
13
14
  columns: [make, model, year, vin, price]
14
15
  attach sale = escrow.hold {
15
16
  payer: actor, payee: dealer
17
+ funding: { controllers: [checkout], reference: "funds", blocking_states: [active] }
16
18
  fee: { seller: 1%, tax: 15% }, deliver_within: 14d, accept_within: 3d
17
19
  expose create as agree_van_purchase
18
20
  expose fund as pay_cash_instead
@@ -40,13 +42,17 @@ object car "Delivery van finance" {
40
42
  // Down payment is 6,900 SAR. Capital is 62,100 SAR; total flat profit is
41
43
  // 5,278.50 SAR. Financing service fee: 0%, tax: 0% on principal repayments.
42
44
  // Supply 36 future monthly dates. Approve limits before collecting and funding.
45
+ attach checkout = purchase.checkout {
46
+ plans: plan, funds: sale, borrower: actor, capital: operator
47
+ expose create as prepare_financed_purchase
48
+ }
43
49
  attach plan = financing.installments {
44
50
  borrower: actor, capital: operator, share: 0%
45
51
  months: 36, pricing: flat_total, profit_rate: 8.5%, down_payment: 10%
46
52
  apply: fines_profit_principal, payoff_rebate: 100%, write_off_after: 90d
47
53
  refund_within: 30d, payoff_quote_expiry: 1h, returned_goods: refund_all
48
54
  max_extension: 60d, max_amendments: 2, amendment_expiry: 7d
49
- funds: sale, limits: limits, portfolio: ceiling
55
+ funds: checkout, limits: limits, portfolio: ceiling
50
56
  expose create as finance
51
57
  expose sign as sign_contract
52
58
  expose collect_down_payment as pay_down_payment
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperscale0/hsx",
3
- "version": "1.0.258",
3
+ "version": "1.0.284",
4
4
  "description": "The strictly typed HSX language: compile total financial programs into canonical UDL.",
5
5
  "keywords": [
6
6
  "hsx",
@@ -40,6 +40,10 @@
40
40
  "types": "./dist/src/cost.d.ts",
41
41
  "default": "./dist/src/cost.js"
42
42
  },
43
+ "./bases": {
44
+ "types": "./dist/src/bases.d.ts",
45
+ "default": "./dist/src/bases.js"
46
+ },
43
47
  "./examples": {
44
48
  "types": "./dist/src/examples.d.ts",
45
49
  "default": "./dist/src/examples.js"
@@ -61,6 +65,7 @@
61
65
  "bin",
62
66
  "dist",
63
67
  "docs",
68
+ "bases",
64
69
  "examples",
65
70
  "std",
66
71
  "src"
@@ -78,14 +83,15 @@
78
83
  "postpack": "bun scripts/pack-exports.ts restore",
79
84
  "prepack": "bun run build && bun scripts/pack-exports.ts apply",
80
85
  "std:bundle": "bun scripts/bundle-std.ts",
81
- "generate": "bun run std:bundle && bun run examples:bundle",
82
- "prepare": "bun run std:bundle && bun run examples:bundle",
86
+ "generate": "bun run std:bundle && bun run examples:bundle && bun run bases:bundle",
87
+ "prepare": "bun run std:bundle && bun run examples:bundle && bun run bases:bundle",
83
88
  "check": "bun run typecheck && tsc -p playground/tsconfig.json && bun test test",
84
- "examples:bundle": "bun scripts/bundle-examples.ts"
89
+ "examples:bundle": "bun scripts/bundle-examples.ts",
90
+ "bases:bundle": "bun scripts/bundle-bases.ts"
85
91
  },
86
92
  "dependencies": {
87
- "@hyperscale0/adl": "1.0.258",
88
- "@hyperscale0/udl": "1.0.258",
93
+ "@hyperscale0/adl": "1.0.284",
94
+ "@hyperscale0/udl": "1.0.284",
89
95
  "fast-sha256": "1.3.0"
90
96
  },
91
97
  "devDependencies": {
@@ -97,5 +103,5 @@
97
103
  "Amir Ayub",
98
104
  "Sara AlBakaawi"
99
105
  ],
100
- "gitHead": "20d3d1eddd70d7629f5ae6a5b14e0e89c4ce536a"
106
+ "gitHead": "f4101f194e0e7c24997b0625ab1d48bec1255508"
101
107
  }
@@ -0,0 +1,15 @@
1
+ // Generated by open/hsx/scripts/bundle-bases.ts. Do not edit by hand.
2
+ import type { HsxBase } from "./bases.ts";
3
+ export const bases: HsxBase[] = [
4
+ {"id":"cards","title":"Control card spending","summary":"Reserve a card payment within its spending limit before capture.","headers":["cards"],"source":"// Reserve a card payment within its spending limit before capture.\nprogram cards_base \"Control card spending\"\nuse cards\nparty seller: business\n\nobject card \"Card\" {\n attach holder = cards.cardholder {\n holder: owner\n expose create as create_holder\n expose activate as activate_holder\n expose suspend as suspend_holder\n expose resume as resume_holder\n expose close as close_holder\n }\n attach card = cards.card {\n holder: holder, person: owner, issuer: operator, spend_limit: runtime\n expose create as create_card\n expose activate as activate_card\n expose freeze as freeze_card\n expose unfreeze as unfreeze_card\n expose issuer_freeze as issuer_freeze_card\n expose issuer_unfreeze as issuer_unfreeze_card\n expose cancel as cancel_card\n }\n attach payment = cards.authorization {\n card: card, merchant: seller, issuer: operator\n expose create as create\n expose approve as approve\n expose decline as decline\n expose capture as capture\n expose reverse as reverse\n }\n attach receipt = cards.transaction {\n authorization: payment\n expose create as record_payment\n expose refund as refund\n }\n}\n"},
5
+ {"id":"collections","title":"Follow up overdue payments","summary":"Refer overdue instalments for collection and limit contact attempts.","headers":["collections","financing"],"source":"// Refer overdue instalments for collection and limit contact attempts.\nprogram collections_base \"Follow up overdue payments\"\nuse collections\nuse financing\nparty collector: business\n\nobject plan \"Plan\" {\n fields { price: money }\n attach allowance = financing.limits {\n borrower: owner, per_borrower: runtime\n expose create as request_limit\n expose approve as approve_limit\n }\n attach budget = financing.portfolio_limit {\n limit: runtime\n expose create as create_budget\n expose approve as approve_budget\n }\n // Architect sets the placeholder profit rate during tailoring.\n attach plan = financing.installments {\n borrower: owner, capital: operator\n months: 2, pricing: flat_total, profit_rate: 0%\n disburse_to: borrower, limits: allowance, portfolio: budget\n expose create as quote\n expose sign as sign\n expose disburse as fund\n expose cancel as cancel_quote\n expose void as cancel_plan\n expose payoff as repay_early\n expose complete as complete\n }\n attach recovery = collections.case {\n on: plan, agency: collector, capital: operator\n expose create as refer\n expose assign as assign\n expose recover as recover\n expose recall as recall\n expose return_uncollected as return_uncollected\n }\n attach contact = collections.contact {\n case: recovery, agency: collector\n expose create as record_contact\n }\n}\n\nexpose plan.plan.payment.create as record_payment\nexpose plan.plan.payment.pay as pay_instalment\nexpose plan.plan.payment.refund as refund_payment\nexpose plan.plan.payoff_quote.create as quote_payoff\n\nexpose plan.contact.outcome.create as record_contact_outcome\n"},
6
+ {"id":"escrow","title":"Hold payment until delivery","summary":"Hold a buyer’s payment until delivery is accepted.","headers":["escrow"],"source":"// Hold a buyer’s payment until delivery is accepted.\nprogram escrow_base \"Hold payment until delivery\"\nuse escrow\nparty seller: business\n\nobject order \"Order\" {\n fields { price: money }\n attach payment = escrow.hold {\n payer: owner, payee: seller\n expose create as create\n expose fund as fund\n expose deliver as deliver\n expose accept as accept\n expose dispute as dispute\n expose verify_return as verify_return\n expose refund as refund\n expose refund_undelivered as refund_undelivered\n expose settle_release as settle_release\n expose settle_return as settle_return\n expose settle_refund as settle_refund\n expose cancel as cancel\n }\n}\n"},
7
+ {"id":"financing","title":"Split a purchase into instalments","summary":"Fund a purchase and collect its scheduled repayments.","headers":["financing"],"source":"// Fund a purchase and collect its scheduled repayments.\nprogram financing_base \"Split a purchase into instalments\"\nuse financing\nparty seller: business\n\nobject purchase \"Purchase\" {\n fields { price: money }\n attach allowance = financing.limits {\n borrower: owner, per_borrower: runtime\n expose create as request_limit\n expose approve as approve_limit\n }\n attach budget = financing.portfolio_limit {\n limit: runtime\n expose create as create_budget\n expose approve as approve_budget\n }\n // Architect sets the placeholder profit rate during tailoring.\n attach plan = financing.installments {\n borrower: owner, capital: operator\n months: 2, pricing: flat_total, profit_rate: 0%\n disburse_to: seller, seller: seller, limits: allowance, portfolio: budget\n expose create as quote\n expose sign as sign\n expose disburse as fund\n expose cancel as cancel_quote\n expose void as cancel_plan\n expose payoff as repay_early\n expose complete as complete\n }\n}\n\nexpose purchase.plan.payment.create as record_payment\nexpose purchase.plan.payment.pay as pay_instalment\nexpose purchase.plan.payment.refund as refund_payment\nexpose purchase.plan.payoff_quote.create as quote_payoff\n"},
8
+ {"id":"insurance","title":"Protect a purchase","summary":"Collect cover premiums and reserve approved claims for payment.","headers":["insurance"],"source":"// Collect cover premiums and reserve approved claims for payment.\nprogram insurance_base \"Protect a purchase\"\nuse insurance\n\nobject purchase \"Purchase\" {\n attach cover = insurance.cover {\n holder: owner, adapter: \"conformance_boundary\"\n expose create as create_cover\n expose activate as activate_cover\n expose cancel as cancel_cover\n }\n attach claim = insurance.claim {\n cover: cover, inspector: operator\n expose create as create_claim\n expose approve as approve_claim\n expose deny as deny_claim\n expose pay as pay_claim\n }\n}\n\nexpose purchase.cover.slice.create as create_period\nexpose purchase.cover.slice.collect as collect_premium\n"},
9
+ {"id":"lending","title":"Pool funds for lending","summary":"Pool member commitments to fund a loan and distribute repayments or losses.","headers":["lending","financing","wallet"],"source":"// Pool member commitments to fund a loan and distribute repayments or losses.\nprogram lending_base \"Pool funds for lending\"\nuse lending\nuse financing\nuse wallet\n\nobject plan \"Plan\" {\n fields { price: money }\n attach allowance = financing.limits {\n borrower: owner, per_borrower: runtime\n expose create as request_limit\n expose approve as approve_limit\n }\n attach budget = financing.portfolio_limit {\n limit: runtime\n expose create as create_budget\n expose approve as approve_budget\n }\n // Architect sets the placeholder profit rate during tailoring.\n attach plan = financing.installments {\n borrower: owner, capital: operator\n months: 2, pricing: flat_total, profit_rate: 0%\n disburse_to: borrower, limits: allowance, portfolio: budget\n expose create as quote\n expose sign as sign\n expose disburse as fund\n expose cancel as cancel_quote\n expose void as cancel_plan\n expose payoff as repay_early\n expose complete as complete\n }\n attach balance = wallet.balance {\n holder: actor\n expose create as create_balance\n expose activate as activate_balance\n expose topup as add_funds\n expose withdraw as withdraw_funds\n expose close as close_balance\n }\n attach round = lending.round {\n borrower: owner, plan: plan\n expose create as open_round\n expose close as close_round\n }\n attach commitment = lending.commitment {\n round: round, wallet: balance, investor: actor\n expose create as commit\n expose withdraw as withdraw_commitment\n }\n attach returns = lending.distribution {\n round: round, receipt: plan.settlement\n expose create as record_settlement\n expose prepare_cash as prepare_returns\n expose distribute_cash as distribute_returns\n }\n attach losses = lending.distribution {\n round: round, receipt: plan.settlement, mode: loss\n expose create as record_loss\n expose prepare_loss as prepare_losses\n expose distribute_loss as distribute_losses\n }\n}\n\nexpose plan.plan.payment.create as record_payment\nexpose plan.plan.payment.pay as pay_instalment\nexpose plan.plan.payment.refund as refund_payment\nexpose plan.plan.payoff_quote.create as quote_payoff\n\nexpose plan.returns.share.create as create_return_share\nexpose plan.losses.share.create as create_loss_share\nexpose plan.plan.write_off_request.create as request_write_off\nexpose plan.plan.write_off_request.apply as apply_write_off\nexpose plan.plan.write_off_request.cancel as cancel_write_off\n"},
10
+ {"id":"marketplace","title":"Connect buyers and sellers","summary":"Turn a seller’s offer into an order with payment held until acceptance.","headers":["marketplace","escrow"],"source":"// Turn a seller’s offer into an order with payment held until acceptance.\nprogram marketplace_base \"Connect buyers and sellers\"\nuse marketplace\nuse escrow\nparty seller: business\n\nobject order \"Order\" {\n fields { price: money }\n attach offer = marketplace.listing {\n seller: seller\n expose create as create_offer\n expose publish as publish_offer\n expose withdraw as withdraw_offer\n expose sell as sell_offer\n }\n attach order = marketplace.order {\n listing: offer, buyer: owner, fulfiller: seller\n expose create as create_order\n expose commit as commit_order\n expose fulfill as fulfill_order\n expose cancel as cancel_order\n }\n attach payment = escrow.hold {\n payer: owner, payee: seller\n expose create as create\n expose fund as fund\n expose deliver as deliver\n expose accept as accept\n expose dispute as dispute\n expose verify_return as verify_return\n expose refund as refund\n expose refund_undelivered as refund_undelivered\n expose settle_release as settle_release\n expose settle_return as settle_return\n expose settle_refund as settle_refund\n expose cancel as cancel\n }\n}\n"},
11
+ {"id":"money","title":"Collect a payment","summary":"Collect a payment from a customer.","headers":["money"],"source":"// Collect a payment from a customer.\nprogram money_base \"Collect a payment\"\nuse money\n\nobject purchase \"Purchase\" {\n attach payment = money.transfer {\n payer: owner, payee: operator\n expose create as create\n expose pay as pay\n expose cancel as cancel\n }\n}\n"},
12
+ {"id":"reporting","title":"Track repayments","summary":"Report outstanding balances and overdue repayments on financed purchases.","headers":["reporting","financing"],"source":"// Report outstanding balances and overdue repayments on financed purchases.\nprogram reporting_base \"Track repayments\"\nuse reporting\nuse financing\n\nobject purchase \"Purchase\" {\n fields { price: money }\n attach allowance = financing.limits {\n borrower: owner, per_borrower: runtime\n expose create as request_limit\n expose approve as approve_limit\n }\n attach budget = financing.portfolio_limit {\n limit: runtime\n expose create as create_budget\n expose approve as approve_budget\n }\n // Architect sets the placeholder profit rate during tailoring.\n attach plan = financing.installments {\n borrower: owner, capital: operator\n months: 2, pricing: flat_total, profit_rate: 0%\n disburse_to: borrower, limits: allowance, portfolio: budget\n expose create as quote\n expose sign as sign\n expose disburse as fund\n expose cancel as cancel_quote\n expose void as cancel_plan\n expose payoff as repay_early\n expose complete as complete\n }\n attach reports = reporting.portfolio {\n on: plan\n expose create as create_reports\n }\n}\n\nexpose purchase.plan.payment.create as record_payment\nexpose purchase.plan.payment.pay as pay_instalment\nexpose purchase.plan.payment.refund as refund_payment\nexpose purchase.plan.payoff_quote.create as quote_payoff\n"},
13
+ {"id":"savings","title":"Save as a group","summary":"Collect contributions into a shared pot and pay each member in turn.","headers":["savings"],"source":"// Collect contributions into a shared pot and pay each member in turn.\nprogram savings_base \"Save as a group\"\nuse savings\n\nobject pot \"Pot\" {\n attach circle = savings.circle {\n contribution: runtime, members: 2, starts: runtime\n expose create as create_circle\n expose disband as disband_circle\n expose close as close_circle\n }\n attach membership = savings.membership {\n circle: circle, member: actor\n expose create as join\n expose activate as activate_membership\n expose withdraw as withdraw\n }\n}\n\nexpose pot.membership.contribution.create as schedule_contribution\n"},
14
+ {"id":"wallet","title":"Offer prepaid credit","summary":"Let a customer add funds and reserve each spend before payment.","headers":["wallet"],"source":"// Let a customer add funds and reserve each spend before payment.\nprogram wallet_base \"Offer prepaid credit\"\nuse wallet\n\nobject balance \"Balance\" {\n attach balance = wallet.balance {\n holder: owner\n expose create as create_balance\n expose activate as activate_balance\n expose topup as add_funds\n expose withdraw as withdraw_funds\n expose freeze as freeze_balance\n expose unfreeze as unfreeze_balance\n expose close as close_balance\n }\n attach spend = wallet.spend {\n wallet: balance, payee: operator, holder: owner\n expose create as create\n expose reserve as reserve\n expose pay as pay\n expose cancel as cancel\n }\n}\n"},
15
+ ];
package/src/bases.ts ADDED
@@ -0,0 +1,9 @@
1
+ /** Neutral programs generated from bases/*.hsx. */
2
+ export interface HsxBase {
3
+ id: string;
4
+ title: string;
5
+ summary: string;
6
+ headers: string[];
7
+ source: string;
8
+ }
9
+ export { bases } from "./bases-bundle.ts";
package/src/compile.ts CHANGED
@@ -241,6 +241,24 @@ const automatic = (actor: UdlAction["actor"]) =>
241
241
  (actor !== null && typeof actor === "object" && "parent" in actor);
242
242
  const title = (name: string) =>
243
243
  name[0]!.toUpperCase() + name.slice(1).replaceAll("_", " ");
244
+ /** Parties an instrument names itself: account owners, actors, move endpoints. */
245
+ function namedParties(instrument: UdlInstrument): string[] {
246
+ const names = instrument.fields.flatMap((field) =>
247
+ field.type === "account" && typeof field.owner === "string"
248
+ ? [field.owner]
249
+ : [],
250
+ );
251
+ for (const action of Object.values(instrument.actions)) {
252
+ if (typeof action.actor === "object" && "party" in action.actor)
253
+ names.push(action.actor.party);
254
+ for (const move of action.moves) {
255
+ if (move.economics) names.push(move.economics.sourceParty);
256
+ for (const endpoint of "from" in move ? [move.from, move.to] : [])
257
+ if (endpoint.startsWith("party.")) names.push(endpoint.slice(6));
258
+ }
259
+ }
260
+ return names;
261
+ }
244
262
  function entries(block: BlockExpr): Map<string, Expr> {
245
263
  const map = new Map<string, Expr>();
246
264
  for (const row of block.entries) {
@@ -283,6 +301,12 @@ function text(expr: Expr): string {
283
301
  );
284
302
  }
285
303
  function decimal(raw: string, scale: number, expr: Expr): string {
304
+ if (raw.startsWith("-"))
305
+ fail(
306
+ expr,
307
+ "literal cannot be negative",
308
+ "write a positive value; a move's from and to set its direction",
309
+ );
286
310
  const [whole = "", fraction = ""] = raw.split(".");
287
311
  if (!/^\d+$/.test(whole) || fraction.length > scale)
288
312
  fail(
@@ -389,6 +413,12 @@ function literal(expr: Expr, expectedType?: string): string | number | boolean {
389
413
  if (expr.kind === "text") return expr.value;
390
414
  if (expr.kind === "name" && ["true", "false"].includes(expr.value))
391
415
  return expr.value === "true";
416
+ if (expr.kind === "name" && expr.value === "runtime")
417
+ fail(
418
+ expr,
419
+ "this value is fixed when the program compiles, so it cannot be runtime",
420
+ "write a literal value",
421
+ );
392
422
  return fail(expr, "expected a literal", "write a typed constant");
393
423
  }
394
424
 
@@ -1453,7 +1483,7 @@ export function compile(
1453
1483
  )
1454
1484
  fail(
1455
1485
  v,
1456
- `${param.key} is outside ${bounds.minimum}..${bounds.maximum}`,
1486
+ `value is outside ${bounds.minimum}..${bounds.maximum}`,
1457
1487
  "choose a value inside the tunable's bounds",
1458
1488
  );
1459
1489
  } catch (error) {
@@ -1524,7 +1554,7 @@ export function compile(
1524
1554
  "name the compared tunables",
1525
1555
  );
1526
1556
  const numericKind = (expr: Expr) =>
1527
- expr.kind === "name" && /^P/.test(expr.value)
1557
+ expr.kind === "name" && /^P(?:\d|T)/.test(expr.value)
1528
1558
  ? "duration"
1529
1559
  : expr.kind;
1530
1560
  if (
@@ -1627,6 +1657,7 @@ export function compile(
1627
1657
  return lowered;
1628
1658
  };
1629
1659
  const fields: UdlField[] = [];
1660
+ const boundaryReceipts = new Map<string, string>();
1630
1661
  const calculations: UdlCalculation[] = [];
1631
1662
  let currentAction: UdlAction | undefined;
1632
1663
  let currentActionName: string | undefined;
@@ -2070,6 +2101,38 @@ export function compile(
2070
2101
  actions: Object.create(null) as UdlInstrument["actions"],
2071
2102
  actionOrder: [],
2072
2103
  };
2104
+ const actionTargets = new Set<string>();
2105
+ const collectTargets = (block: BlockExpr): void => {
2106
+ for (const entry of block.entries)
2107
+ if (entry.key.startsWith("when "))
2108
+ collectTargets(asBlock(entry.value));
2109
+ else if (entry.key === "calculate")
2110
+ for (const item of entry.value.kind === "list"
2111
+ ? entry.value.items
2112
+ : [entry.value]) {
2113
+ const target =
2114
+ item.kind === "block" ? entries(item).get("target") : undefined;
2115
+ if (target) actionTargets.add(String(data(target)));
2116
+ }
2117
+ };
2118
+ for (const row of decl.body.entries)
2119
+ if (row.key.startsWith("action ")) collectTargets(asBlock(row.value));
2120
+ // Instrument calculations run before every action, create included, so
2121
+ // a fee or split calculates there only on an amount held from create.
2122
+ const heldFromCreate = (amount: UdlValue): boolean => {
2123
+ if (!("field" in amount)) return true;
2124
+ const [root, name, ...rest] = amount.field.split(".");
2125
+ const field = fields.find((item) => item.name === name);
2126
+ return (
2127
+ root === "self" &&
2128
+ !rest.length &&
2129
+ !!field &&
2130
+ !actionTargets.has(name!) &&
2131
+ (!field.optional ||
2132
+ "value" in field ||
2133
+ calculations.some((node) => node.target === name))
2134
+ );
2135
+ };
2073
2136
  for (const row of decl.body.entries.filter((e) =>
2074
2137
  e.key.startsWith("action "),
2075
2138
  )) {
@@ -2407,7 +2470,7 @@ export function compile(
2407
2470
  ...(actionSubject ? { subject: actionSubject } : {}),
2408
2471
  };
2409
2472
  authored.set(a, row.value);
2410
- const actionFeeCalculations: UdlCalculation[] = [];
2473
+ const moveCalculations: UdlCalculation[] = [];
2411
2474
  for (const binding of boundaryBindings)
2412
2475
  if (
2413
2476
  !a.subject?.adapters.find((entry) => entry.binding === binding)
@@ -2447,12 +2510,28 @@ export function compile(
2447
2510
  const op = parts.has("operation")
2448
2511
  ? String(data(parts.get("operation")!))
2449
2512
  : "internal_transfer.create";
2450
- if (op === "internal_transfer.reserve" && !parts.has("capture"))
2451
- fail(
2452
- move,
2453
- "reserve needs a receipt field",
2454
- "add capture: receipt_name",
2455
- );
2513
+ if (op === "internal_transfer.reserve" && !parts.has("capture")) {
2514
+ if (!parts.has("boundary"))
2515
+ fail(
2516
+ move,
2517
+ "reserve needs a receipt field",
2518
+ "add capture: receipt_name",
2519
+ );
2520
+ const capture = `${name}Receipt`;
2521
+ if (fields.some((field) => field.name === capture))
2522
+ fail(
2523
+ move,
2524
+ "boundary receipt name is already used",
2525
+ "name each reservation with an explicit capture",
2526
+ );
2527
+ fields.push({ name: capture, type: "text", optional: true });
2528
+ boundaryReceipts.set(name, capture);
2529
+ parts.set("capture", {
2530
+ kind: "text",
2531
+ value: capture,
2532
+ span: move.span,
2533
+ });
2534
+ }
2456
2535
  if (
2457
2536
  (op === "internal_transfer.post" ||
2458
2537
  op === "internal_transfer.void") &&
@@ -2643,6 +2722,7 @@ export function compile(
2643
2722
  );
2644
2723
  let total = 0;
2645
2724
  const pieces: UdlValue[] = [];
2725
+ const perAction = !heldFromCreate(val(amount));
2646
2726
  for (const [index, [party, rate]] of shares.entries()) {
2647
2727
  const recipient = resolve({
2648
2728
  kind: "name",
@@ -2694,8 +2774,12 @@ export function compile(
2694
2774
  "split field name conflicts",
2695
2775
  "rename the move key",
2696
2776
  );
2697
- fields.push({ name: target, type: "money" });
2698
- calculations.push(
2777
+ fields.push({
2778
+ name: target,
2779
+ type: "money",
2780
+ ...(perAction ? { optional: true } : {}),
2781
+ });
2782
+ (perAction ? moveCalculations : calculations).push(
2699
2783
  index === shares.length - 1 && pieces.length
2700
2784
  ? {
2701
2785
  target,
@@ -2797,6 +2881,22 @@ export function compile(
2797
2881
  "write seller: 1% or buyer: 1%",
2798
2882
  );
2799
2883
  const quoted = terms.get(paidBy)!;
2884
+ // An account the agreement owns holds only what was moved
2885
+ // into it, so a charge on top has nothing to come from.
2886
+ if (
2887
+ paidBy === "buyer" &&
2888
+ fields.some(
2889
+ (field) =>
2890
+ `self.${field.name}` === transfer.from &&
2891
+ field.type === "account" &&
2892
+ field.owner === "self",
2893
+ )
2894
+ )
2895
+ fail(
2896
+ quoted,
2897
+ `a buyer fee cannot come from \`${transfer.from.slice("self.".length)}\`, which holds only the moved amount`,
2898
+ "use a seller fee, which comes out of the held amount",
2899
+ );
2800
2900
  const rate = quoted.kind === "capped" ? quoted.rate : quoted;
2801
2901
  if (rate.kind !== "percent")
2802
2902
  fail(
@@ -2805,11 +2905,7 @@ export function compile(
2805
2905
  "write a percentage such as 1%",
2806
2906
  );
2807
2907
  const prefix = `${name}_${key}`;
2808
- // Instrument calculations run at create, before any action
2809
- // input exists, so a fee on an input amount runs with its action.
2810
- const perAction =
2811
- "field" in transfer.amount &&
2812
- transfer.amount.field.startsWith("input.");
2908
+ const perAction = !heldFromCreate(transfer.amount);
2813
2909
  const derived = (
2814
2910
  suffix: string,
2815
2911
  label: string,
@@ -2828,7 +2924,7 @@ export function compile(
2828
2924
  label,
2829
2925
  ...(perAction ? { optional: true } : {}),
2830
2926
  });
2831
- (perAction ? actionFeeCalculations : calculations).push({
2927
+ (perAction ? moveCalculations : calculations).push({
2832
2928
  ...calculation,
2833
2929
  target,
2834
2930
  } as UdlCalculation);
@@ -2960,8 +3056,8 @@ export function compile(
2960
3056
  }
2961
3057
  } else (a as unknown as Record<string, unknown>)[key] = data(expr);
2962
3058
  }
2963
- if (actionFeeCalculations.length)
2964
- a.calculate = [...(a.calculate ?? []), ...actionFeeCalculations];
3059
+ if (moveCalculations.length)
3060
+ a.calculate = [...(a.calculate ?? []), ...moveCalculations];
2965
3061
  if (attachmentInfo) {
2966
3062
  if (attachmentInfo.exposed.has(name)) {
2967
3063
  if (automatic(a.actor))
@@ -3060,6 +3156,14 @@ export function compile(
3060
3156
  inst.actions[name] = a;
3061
3157
  inst.actionOrder.push(name);
3062
3158
  }
3159
+ for (const action of Object.values(inst.actions))
3160
+ for (const move of action.moves)
3161
+ if ("transfer" in move) {
3162
+ const receipt = boundaryReceipts.get(
3163
+ move.transfer.replace(/^self\./, ""),
3164
+ );
3165
+ if (receipt) move.transfer = `self.${receipt}`;
3166
+ }
3063
3167
  if (attachmentInfo && !attachmentInfo.child)
3064
3168
  applyAttachmentEconomics(
3065
3169
  document,
@@ -3339,6 +3443,21 @@ export function compile(
3339
3443
  bindingDiagnostics.push(error.diagnostic);
3340
3444
  continue;
3341
3445
  }
3446
+ // A business the instrument names itself binds like a parameter, so
3447
+ // Build asks for it and the engine resolves its account and principal.
3448
+ // programOperator and programTax are the company's own accounts.
3449
+ for (const party of document.instruments
3450
+ .slice(firstAttachedInstrument)
3451
+ .flatMap(namedParties))
3452
+ if (
3453
+ document.parties[party]?.kind === "business" &&
3454
+ party !== "programOperator" &&
3455
+ party !== "programTax" &&
3456
+ !Object.values(parties).some(
3457
+ (binding) => "party" in binding && binding.party === party,
3458
+ )
3459
+ )
3460
+ parties[party] ??= { party };
3342
3461
  const attachedInst = document.instruments.find((i) => i.id === instId);
3343
3462
  for (const createdInst of document.instruments
3344
3463
  .slice(firstAttachedInstrument)