@hyperscale0/hsx 1.0.124 → 1.0.128

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.
@@ -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,WAAW,EAAE,2oKAA2oK,CAAC;IAC1pK,CAAC,iBAAiB,EAAE,2sNAA2sN,CAAC;IAChuN,CAAC,YAAY,EAAE,+qHAA+qH,CAAC;IAC/rH,CAAC,eAAe,EAAE,6m+DAA6m+D,CAAC;IACho+D,CAAC,eAAe,EAAE,01KAA01K,CAAC;IAC72K,CAAC,aAAa,EAAE,qwTAAqwT,CAAC;IACtxT,CAAC,iBAAiB,EAAE,woTAAwoT,CAAC;IAC7pT,CAAC,WAAW,EAAE,u1VAAu1V,CAAC;IACt2V,CAAC,eAAe,EAAE,+q2BAA+q2B,CAAC;IACls2B,CAAC,aAAa,EAAE,85JAA85J,CAAC;IAC/6J,CAAC,YAAY,EAAE,2jGAA2jG,CAAC;CAC5kG,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,soLAAsoL,CAAC;IACvpL,CAAC,WAAW,EAAE,2oKAA2oK,CAAC;IAC1pK,CAAC,iBAAiB,EAAE,2sNAA2sN,CAAC;IAChuN,CAAC,YAAY,EAAE,i9JAAi9J,CAAC;IACj+J,CAAC,eAAe,EAAE,uzpEAAuzpE,CAAC;IAC10pE,CAAC,eAAe,EAAE,uxLAAuxL,CAAC;IAC1yL,CAAC,aAAa,EAAE,qwTAAqwT,CAAC;IACtxT,CAAC,iBAAiB,EAAE,mpTAAmpT,CAAC;IACxqT,CAAC,WAAW,EAAE,+1VAA+1V,CAAC;IAC92V,CAAC,eAAe,EAAE,+q2BAA+q2B,CAAC;IACls2B,CAAC,aAAa,EAAE,85JAA85J,CAAC;IAC/6J,CAAC,YAAY,EAAE,kyHAAkyH,CAAC;CACnzH,CAAC,CAAC"}
package/docs/README.md CHANGED
@@ -82,11 +82,16 @@ the executor's current time when the action runs, not a caller-supplied return d
82
82
  A late `due` permits the operator action; it does not schedule that action.
83
83
 
84
84
  This sample binds a declared business named `renter` in Product configuration.
85
- It uses fixed parties because this compiler rejects parameterized instruments
86
- inside a program. Changing the renter binding is a Build configuration decision,
87
- not a different party ID on each request. Use header party parameters bound to
88
- `owner` or `actor` for per-object participants. Reusable custom headers require a
89
- host-supplied `StandardLibrary`; `use` cannot load a local file through this CLI.
85
+ The renter is one business for the whole Build, so the instrument takes no
86
+ parameters. Changing the renter binding is a Build configuration decision, not a
87
+ different party ID on each request. For per-object participants, give a program
88
+ instrument parameters as a header would, such as `instrument purse(holder: party)`,
89
+ and bind `holder: owner` or `holder: actor` in each `attach` block. A
90
+ parameterized program instrument exists only through its attachments. A `fee` on
91
+ a move of an action input, such as `moves input.amount from holder to self.held
92
+ fee { seller: 1% }`, is calculated when that action runs. Reusable custom headers
93
+ require a host-supplied `StandardLibrary`; `use` cannot load a local file through
94
+ this CLI.
90
95
 
91
96
  Inspect `objects` in the compiled UDL for the attachment and public names, then
92
97
  its instrument for fields, transitions, actors, requirements and ordered moves.
@@ -104,11 +109,15 @@ executor. Compilation cannot establish balances, permissions or provider readine
104
109
 
105
110
  Every attached party parameter binds to `owner`, `actor`, `operator`, a declared
106
111
  business, or a declared staff party with a supported role. Configure declared
107
- business participants per Build. Declared person parties cannot bind attachments.
112
+ business participants per Build. The host binds a declared business to an
113
+ organization customer that holds one open balance account in the Product, so the
114
+ Product exists before the binding and a recomposition adds it. Declared person
115
+ parties cannot bind attachments.
108
116
  `owner` is the object's resolved customer or business, `actor` is the authenticated
109
117
  caller, and `operator` is the program operator. A parameter with one of these
110
- names binds by name. Other party parameters use a same-named eligible declaration,
111
- a header default, or an explicit binding. Explicit bindings take precedence.
118
+ names binds by name. Other required party parameters use a same-named eligible
119
+ declaration, a header default, or an explicit binding. An optional party parameter
120
+ stays unset unless the attachment binds it. Explicit bindings take precedence.
112
121
  Unbound parameters report `subject_party_unbound`. These bindings are frozen in
113
122
  UDL `objects[].attachments[].parties`; no caller supplies party IDs at creation.
114
123
 
@@ -156,6 +165,8 @@ decimal places and percentages at most two. The compiler uses integer minor unit
156
165
  and basis points. Fee and tax calculations round down. Split percentages sum to
157
166
  100%; the last declared recipient receives the rounding remainder. Finite
158
167
  schedules use positions 1 through n and put the division remainder in position 1.
168
+ Amortizing financing is the exception: it rounds half up and puts the remainder in
169
+ the last instalment, as a standard amortization table does.
159
170
  Tax binds `programTax`. Fees default to `programOperator`. Fine, recovery and
160
171
  residual destinations are party tunables.
161
172
 
@@ -261,20 +272,30 @@ and a correction. No artifact is returned with diagnostics.
261
272
 
262
273
  ## Financing policies
263
274
 
264
- `financing.installments` keeps principal and fixed flat profit as claims from
265
- activation. The company chooses where funding goes and when profit becomes earned.
266
- All choices use accounts, calculations and moves in the same two books.
267
-
268
- | Tunable | Default | Selected behavior |
269
- | ---------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
270
- | `disburse_to` | `funds` | `funds` pays down payment and principal into the hold and confirms it. `borrower` credits principal to the borrower's balance. |
271
- | `profit_earned` | `on_payment` | `on_payment` collects unearned profit on payment. `by_schedule` moves unearned claims to earned claims at each slice's date. `at_disbursement` makes that move at activation. Cash arrives only on payment. |
272
- | `apply` | `fines_profit_principal` | Visit each slice in position order; within that slice collect assessed fines by overdue date, evidenced costs, profit, then principal. `principal_profit` collects each slice in position order, principal before profit. `pro_rata` uses the slice's fixed principal:profit ratio, caps each side by its collectible balance, and assigns the remainder without stranding cash. |
273
- | `payoff_rebate` | `100%` | Reverse this share of unearned profit to borrower debt. Collect the remainder and all earned profit with outstanding principal. |
274
- | `late_charge.fines_to` | `programOperator` | Receive fine cash and fund its refunds. |
275
- | `late_charge.costs_to` | `programOperator` | Receive evidenced recovery cash and fund its refunds. |
276
- | `collections.case.fee` | `20%` | After payment succeeds, transfer this share of the payment amount from plan capital to the agency. |
277
- | `lending.distribution.residual_to` | `programOperator` | Receive the cash remainder and own the claim loss remainder after weighted distribution. |
275
+ `financing.installments` keeps principal and fixed profit as claims from
276
+ activation. The company chooses how profit is priced, where funding goes and when
277
+ profit becomes earned. All choices use accounts, calculations and moves in the same
278
+ two books.
279
+
280
+ `pricing` has no default, so every plan states it. `amortizing` reads
281
+ `profit_rate` as an annual rate on the declining balance: equal monthly
282
+ instalments from standard annuity maths, interest and instalment rounded half up,
283
+ the last instalment absorbing the rounding. 12,000 SAR at 18% over 12 months is
284
+ 11 instalments of 1,100.16 SAR and a last one of 1,100.19 SAR, 13,201.95 SAR in
285
+ total. `flat_total` reads `profit_rate` as one charge on principal for the whole
286
+ term, rounded down and split evenly with the remainder in position 1: murabaha and
287
+ fixed-fee pay-later. The same 12,000 SAR at 18% `flat_total` costs 14,160 SAR.
288
+
289
+ | Tunable | Default | Selected behavior |
290
+ | ---------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
291
+ | `disburse_to` | `funds` | `funds` pays down payment and principal into the hold and confirms it. `borrower` credits principal to the borrower's balance. `seller` pays principal to the `seller` party at activation; after capital refunds every paid payment, capital's `unwind` debits it back from the seller and cancels open slices. |
292
+ | `profit_earned` | `on_payment` | `on_payment` collects unearned profit on payment. `by_schedule` moves unearned claims to earned claims at each slice's date. `at_disbursement` makes that move at activation. Cash arrives only on payment. |
293
+ | `apply` | `fines_profit_principal` | Visit each slice in position order; within that slice collect assessed fines by overdue date, evidenced costs, profit, then principal. `principal_profit` collects each slice in position order, principal before profit. `pro_rata` uses the slice's fixed principal:profit ratio (its own principal and profit under `amortizing`), caps each side by its collectible balance, and assigns the remainder without stranding cash. |
294
+ | `payoff_rebate` | `100%` | Reverse this share of unearned profit to borrower debt. Collect the remainder and all earned profit with outstanding principal. |
295
+ | `late_charge.fines_to` | `programOperator` | Receive fine cash and fund its refunds. |
296
+ | `late_charge.costs_to` | `programOperator` | Receive evidenced recovery cash and fund its refunds. |
297
+ | `collections.case.fee` | `20%` | After payment succeeds, transfer this share of the payment amount from plan capital to the agency. |
298
+ | `lending.distribution.residual_to` | `programOperator` | Receive the cash remainder and own the claim loss remainder after weighted distribution. |
278
299
 
279
300
  Each slice owns separate unearned and earned claim accounts. Scheduled recognition
280
301
  uses a dated child record so a partial payment cannot disable the maturity clock.
@@ -362,9 +383,9 @@ The compiler API is `compile(source)`. On success, `result.verdict` is `valid`
362
383
  and `result.artifacts` contains `document`, `costManifest` and `originMap`.
363
384
  On refusal, inspect `result.diagnostics`; no artifact is returned.
364
385
 
365
- `hsx headers` prints the instrument inventory from the declarations. The
386
+ `hsx headers --json` prints the instrument inventory from the declarations. The
366
387
  [object example](../examples/library.hsx) shows authenticated role bindings.
367
- The [playground](../playground/index.html) compiles locally in the browser.
388
+ The [playground](https://hyperscale0.ai/playground/) compiles locally in the browser.
368
389
 
369
390
  ## Standard-library behavior
370
391
 
@@ -372,18 +393,21 @@ Read the instrument's states, time gates and moves before promising a money
372
393
  outcome. A compiler pass does not prove that required actions are exposed,
373
394
  adapters are bound, participants have funds, or the flow can finish.
374
395
 
375
- | Instrument | Behavior |
376
- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
377
- | `financing.installments` | `share` allocates collected profit to a cash payable owned by `capital`. It does not itself pay operator income. |
378
- | `financing.limits` | `per_borrower` caps outstanding principal. Both borrower and portfolio limits must be approved before disbursement; attaching them does not approve them. |
379
- | `lending.round` | The target is the linked plan's principal. Closing requires commitments and held funds to equal that amount. |
380
- | `lending.distribution` | Cash distribution needs an eligible recorded settlement, `prepare_cash`, one share record per funded commitment, then `distribute_cash`. Attaching it moves nothing. |
381
- | `insurance.cover.slice` | `collect` credits premium net of commission to the insurer account. The tenant retains commission. `refund` returns both portions; claims reserve insurer funds. External settlement uses the boundary protocol. |
382
- | `financing.installments`, `savings.circle` | Supply explicit date lists when creating agreements. A term count does not generate a monthly calendar. Savings supports at most 60 distinct member seats. |
383
- | `escrow.hold` | `fund` takes the whole price. Financing into pending escrow collects the remaining down payment and adds capital principal at disbursement; `fund` is not a down-payment checkout. |
384
- | `escrow.hold` | Acceptance timeout enters `disputed` without paying the seller. Delivery and return verification belong to `payee`; rebinding it also changes who receives accepted funds. |
385
- | `financing.late_charge` | `fine` is a fixed money amount, not a percentage of overdue principal. |
386
- | `cards.card` | `spend_limit` is a per-authorization ceiling, not a monthly aggregate. |
396
+ | Instrument | Behavior |
397
+ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
398
+ | `financing.installments` | `share` allocates collected profit to a cash payable owned by `capital`. It does not itself pay operator income. |
399
+ | `financing.limits` | `per_borrower` caps outstanding principal. Both borrower and portfolio limits must be approved before disbursement; attaching them does not approve them. |
400
+ | `lending.round` | The target is the linked plan's principal. Closing requires commitments and held funds to equal that amount. |
401
+ | `lending.distribution` | Cash distribution needs an eligible recorded settlement, `prepare_cash`, one share record per funded commitment, then `distribute_cash`. Attaching it moves nothing. |
402
+ | `insurance.cover.slice` | `collect` credits premium net of commission to the insurer account. The tenant retains commission. `refund` returns both portions; claims reserve insurer funds. External settlement uses the boundary protocol. |
403
+ | `financing.installments`, `savings.circle` | Supply explicit date lists when creating agreements. A term count does not generate a monthly calendar. Savings supports at most 60 distinct member seats. |
404
+ | `escrow.hold` | `fund` takes the whole price. Financing into pending escrow collects the remaining down payment and adds capital principal at disbursement; `fund` is not a down-payment checkout. |
405
+ | `escrow.hold` | Acceptance timeout enters `disputed` without paying the seller. Delivery and return verification belong to `payee`; rebinding it also changes who receives accepted funds. |
406
+ | `escrow.hold` | `deliver_within` (default `14d`) starts at funding. After it passes without delivery, an exposed `refund_undelivered` lets the payer take the whole held price back. Nothing runs it automatically. |
407
+ | `wallet.balance` | `topup_fee` (default `0%`) takes a floored percentage of each top-up for the Company. At `1%`, a 100.00 top-up credits 99.00 and earns 1.00. |
408
+ | `booking.reservation` | The balance is payable only from `deposit_paid`, so a cancelled booking never takes it. `middle_forfeit` and `late_forfeit` default to `0%`, so every cancellation returns all held money until the author sets them. The clock completes a paid booking at departure. |
409
+ | `financing.late_charge` | `fine` is a fixed money amount, not a percentage of overdue principal. |
410
+ | `cards.card` | `spend_limit` is a per-authorization ceiling, not a monthly aggregate. |
387
411
 
388
412
  A seller-owned marketplace object can declare `entryActions: [create_order]`
389
413
  when `create_order` exposes an attachment's create action with its buyer bound
@@ -441,8 +465,9 @@ has no checkout collection action. The plan and escrow must agree on payer and
441
465
  price.
442
466
 
443
467
  Expose escrow `cancel` to return money credited before full funding. Plan `void`
444
- cancels the signed financing agreement but does not cancel its escrow. After
445
- funding and delivery, the default return path needs three separate aliases for
468
+ cancels the signed financing agreement but does not cancel its escrow. Between
469
+ funding and delivery, the payee's `refund` returns the full price. After
470
+ delivery, the default return path needs three separate aliases for
446
471
  `dispute`, `verify_return` and `refund`. The payee supplies return evidence and
447
472
  returns the full price to the payer. `accept` pays the payee instead.
448
473
  Marketplace order fulfillment alone does not execute any of these money actions.
@@ -458,7 +483,10 @@ Bind `insurance.cover.broker` to the agency that receives commission. Collection
458
483
  pays the insurer's net premium and the broker's commission as two moves. Refunds
459
484
  reverse those same portions. The default broker is the program operator.
460
485
  `claim.deny` accepts only `submitted`; after approval, `pay` or expiry consumes
461
- the reservation.
486
+ the reservation. `claim_limit` is the sum insured, supplied per cover at creation:
487
+ approved and paid claims on one cover never total more. `adapter` names a
488
+ registered boundary adapter, `conformance_boundary` in sandbox. `covers` is
489
+ optional; the attached object is already the insured thing.
462
490
 
463
491
  A booking cancellation refunds only its own held cash. Expose `cover.cancel`
464
492
  separately for eligible premium refunds before the paid period starts. Active
@@ -497,8 +525,9 @@ instruments stay generic. New Builds carry the purpose; existing agreements
497
525
  and postings keep their retained Build and classification.
498
526
 
499
527
  Held customer funding and its unspent return are `principal`. A release for the
500
- company's sale is `earning`. A payment from company cash to another participant
501
- is `participant_payout`. Claims, waivers, write-offs and own-account allocations
528
+ company's sale is `earning`; its release to another participant is
529
+ `pass_through`, because the company never owned it. A payment from company cash
530
+ to another participant is `participant_payout`. Claims, waivers, write-offs and own-account allocations
502
531
  are `internal`; prepaid balance topups and returns are `prepaid_credit`. Tax is
503
532
  `pass_through`, excluded from earnings and costs. Unpaid claims count as neither.
504
533
  Refunds of paid profit, company charges and commission capture the original
@@ -511,16 +540,14 @@ product fees. Automatic platform fees keep their commercial classification.
511
540
 
512
541
  A move's economics block can select by a resolved party: `economics { recipient:
513
542
  recipient, company: { purpose: earning, sourceParty: payer }, participant:
514
- { purpose: participant_payout, sourceParty: programOperator } }`. The compiler
543
+ { purpose: pass_through, sourceParty: programOperator } }`. The compiler
515
544
  chooses `company` for `operator`, `programOperator`, or a declared party with the
516
545
  `program_operator` role; it chooses `participant` for other bound parties.
517
546
  Both branches must be supplied. A branch may be `unclassified` when no purpose
518
547
  is established. Outside financing-profit and recovery recipients refund directly
519
- to the customer, not the company that funded the payout; those refund branches
520
- remain unclassified because they cannot reverse that company payout. Std
521
- recovery charges use this rule to avoid counting company receipts as participant
522
- costs. Direct customer payments to
523
- outside recipients are pass-through; payments from held company cash are costs.
548
+ to the customer; those refund branches remain unclassified. Customer cash paid to
549
+ outside recipients is pass-through, whether paid direct or released from a hold
550
+ the customer funded as principal. Only company-owned cash paid out is a cost.
524
551
 
525
552
  Generic transfers and mixed principal/profit distributions remain unclassified
526
553
  when their structure does not establish a purpose. Adapter-owned premium and
@@ -12,7 +12,7 @@ object facility "Business loan" {
12
12
  attach loan = financing.installments {
13
13
  expose create as create_loan
14
14
  borrower: owner, capital: operator
15
- months: 12, profit: 6%, disburse_to: borrower
15
+ months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower
16
16
  limits: allowance, portfolio: budget
17
17
  expose sign as sign_loan
18
18
  expose disburse as release_loan
@@ -18,7 +18,7 @@ object enrolment "Enrolment" {
18
18
  attach tuition = financing.installments {
19
19
  expose create as create_tuition
20
20
  borrower: owner, capital: operator
21
- months: 3, profit: 0%, disburse_to: borrower
21
+ months: 3, pricing: flat_total, profit_rate: 0%, disburse_to: borrower
22
22
  limits: allowance, portfolio: budget
23
23
  expose sign as sign_tuition_plan
24
24
  expose disburse as fund_tuition
@@ -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,17 @@ 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. claim_limit is the sum insured, set per cover at creation.
17
17
  attach protection = insurance.cover {
18
18
  expose create as create_protection
19
- holder: owner, adapter: "device_insurer", commission: 10%, covers: purchase
19
+ holder: owner, adapter: "conformance_boundary", commission: 10%, covers: purchase
20
20
  expose activate as activate_cover
21
21
  expose cancel as cancel_cover
22
22
  }
23
23
  attach claim = insurance.claim {
24
24
  expose create as create_claim
25
- cover: protection, inspector: inspector
25
+ cover: protection, inspector: operator
26
26
  expose approve as approve_claim
27
27
  expose deny as deny_claim
28
28
  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.124",
3
+ "version": "1.0.128",
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.124",
88
- "@hyperscale0/udl": "1.0.124",
87
+ "@hyperscale0/adl": "1.0.128",
88
+ "@hyperscale0/udl": "1.0.128",
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": "8275c2a026b6cc8a628ab11198ce0eefb8568982"
100
+ "gitHead": "d22a6727cbbb9fc59ffc1625b2d162711dccbe98"
101
101
  }
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(
@@ -2286,12 +2287,12 @@ export function compile(
2286
2287
  };
2287
2288
  }
2288
2289
  }
2289
- const fromBinding = slots.get("from");
2290
- if (
2291
- fromBinding?.kind === "name" &&
2292
- fromBinding.value.includes(".") &&
2293
- slots.has("moves")
2294
- ) {
2290
+ const from = slots.get("from");
2291
+ // A policy member may sit in a state list such as `[funded, dispute.refund_after]`.
2292
+ const fromBinding = (from?.kind === "list" ? from.items : [from]).find(
2293
+ (item) => item?.kind === "name" && item.value.includes("."),
2294
+ );
2295
+ if (fromBinding?.kind === "name" && slots.has("moves")) {
2295
2296
  const [parameter, member] = fromBinding.value.split(".");
2296
2297
  const suppliedPolicy = supplied.get(parameter!);
2297
2298
  if (suppliedPolicy?.kind === "block") {
@@ -2332,6 +2333,7 @@ export function compile(
2332
2333
  ...(actionSubject ? { subject: actionSubject } : {}),
2333
2334
  };
2334
2335
  authored.set(a, row.value);
2336
+ const actionFeeCalculations: UdlCalculation[] = [];
2335
2337
  for (const binding of boundaryBindings)
2336
2338
  if (
2337
2339
  !a.subject?.adapters.find((entry) => entry.binding === binding)
@@ -2692,8 +2694,14 @@ export function compile(
2692
2694
  "write a percentage such as 1%",
2693
2695
  );
2694
2696
  const prefix = `${name}_${key}`;
2697
+ // Instrument calculations run at create, before any action
2698
+ // input exists, so a fee on an input amount runs with its action.
2699
+ const perAction =
2700
+ "field" in transfer.amount &&
2701
+ transfer.amount.field.startsWith("input.");
2695
2702
  const derived = (
2696
2703
  suffix: string,
2704
+ label: string,
2697
2705
  calculation: Omit<UdlCalculation, "target">,
2698
2706
  ): UdlValue => {
2699
2707
  const target = prefix + "_" + suffix;
@@ -2703,19 +2711,32 @@ export function compile(
2703
2711
  `generated fee field ${target} conflicts with a field`,
2704
2712
  "rename the authored field or move key",
2705
2713
  );
2706
- fields.push({ name: target, type: "money" });
2707
- calculations.push({
2714
+ fields.push({
2715
+ name: target,
2716
+ type: "money",
2717
+ label,
2718
+ ...(perAction ? { optional: true } : {}),
2719
+ });
2720
+ (perAction ? actionFeeCalculations : calculations).push({
2708
2721
  ...calculation,
2709
2722
  target,
2710
2723
  } as UdlCalculation);
2711
2724
  return { field: `self.${target}` };
2712
2725
  };
2713
- const gross = derived("feeGross", {
2714
- op: "rate",
2715
- base: transfer.amount,
2716
- bps: { literal: literal(rate) },
2717
- rounding: "floor",
2718
- } as Omit<UdlCalculation, "target">);
2726
+ // Labels come from the source keys: seller or buyer, cap, tax.
2727
+ const feeLabel = `${paidBy === "seller" ? "Seller" : "Buyer"} fee`;
2728
+ const gross = derived(
2729
+ "feeGross",
2730
+ quoted.kind === "capped"
2731
+ ? `${feeLabel} before cap`
2732
+ : feeLabel,
2733
+ {
2734
+ op: "rate",
2735
+ base: transfer.amount,
2736
+ bps: { literal: literal(rate) },
2737
+ rounding: "floor",
2738
+ } as Omit<UdlCalculation, "target">,
2739
+ );
2719
2740
  let charge = gross;
2720
2741
  if (quoted.kind === "capped") {
2721
2742
  if (quoted.cap.kind !== "money")
@@ -2724,7 +2745,7 @@ export function compile(
2724
2745
  "fee cap needs money",
2725
2746
  "write an amount such as 750 SAR",
2726
2747
  );
2727
- charge = derived("fee", {
2748
+ charge = derived("fee", feeLabel, {
2728
2749
  op: "minimum",
2729
2750
  values: [gross, { literal: literal(quoted.cap) }],
2730
2751
  } as Omit<UdlCalculation, "target">);
@@ -2737,7 +2758,7 @@ export function compile(
2737
2758
  "write tax: 15%",
2738
2759
  );
2739
2760
  const vat = tax
2740
- ? derived("tax", {
2761
+ ? derived("tax", "Fee tax", {
2741
2762
  op: "rate",
2742
2763
  base: charge,
2743
2764
  bps: { literal: literal(tax) },
@@ -2747,7 +2768,7 @@ export function compile(
2747
2768
  const charges = vat ? [charge, vat] : [charge];
2748
2769
  const net =
2749
2770
  paidBy === "seller"
2750
- ? derived("net", {
2771
+ ? derived("net", "Amount after fee", {
2751
2772
  op: "subtract",
2752
2773
  base: transfer.amount,
2753
2774
  subtract: charges,
@@ -2819,6 +2840,8 @@ export function compile(
2819
2840
  }
2820
2841
  } else (a as unknown as Record<string, unknown>)[key] = data(expr);
2821
2842
  }
2843
+ if (actionFeeCalculations.length)
2844
+ a.calculate = [...(a.calculate ?? []), ...actionFeeCalculations];
2822
2845
  if (attachmentInfo && !attachmentInfo.child) {
2823
2846
  if (attachmentInfo.exposed.has(name)) {
2824
2847
  if (automatic(a.actor))
@@ -3298,7 +3321,9 @@ export function compile(
3298
3321
  });
3299
3322
  };
3300
3323
  for (const decl of program.decls) {
3301
- if (decl.kind === "instrument") {
3324
+ // A parameterized program instrument is a template, like a header
3325
+ // instrument: each attachment instantiates it with its own bindings.
3326
+ if (decl.kind === "instrument" && !decl.parameters.length) {
3302
3327
  addInstrument(decl, decl.name, emptyBlock, decl.span);
3303
3328
  }
3304
3329
  if (decl.kind === "assignment") {
@@ -3638,7 +3663,9 @@ export function compile(
3638
3663
  fail(
3639
3664
  at,
3640
3665
  `\`${name}\` is not a declared party.${didYouMean(name, names)}`,
3641
- `Declare it with \`party ${name}: business\`, or name a declared party (${names.join(", ")}).`,
3666
+ subjectPartyRoles.includes(name as SubjectPartyRole)
3667
+ ? `Declare a party parameter such as \`holder: party\` on the instrument, use it here, and bind \`holder: ${name}\` where you attach it.`
3668
+ : `Declare it with \`party ${name}: business\`, or name a declared party (${names.join(", ")}).`,
3642
3669
  );
3643
3670
  };
3644
3671
  const checkInput = (
@@ -3716,6 +3743,42 @@ export function compile(
3716
3743
  inst,
3717
3744
  label: declarations.get(inst)!.name,
3718
3745
  });
3746
+ // A family member drops an optional reference field its binding left
3747
+ // unset (a borrower-funded plan has no `funds`), so it cannot match and
3748
+ // leaves the selection. The header must still declare the field.
3749
+ const matchable = (selection: UdlSelection) => {
3750
+ const members = [selection.instrument].flat();
3751
+ const header = selection.family
3752
+ ? templates.get(
3753
+ `${selection.family.module}.${selection.family.exportPath}`,
3754
+ )
3755
+ : undefined;
3756
+ if (
3757
+ !header ||
3758
+ !entries(asBlock(entries(header.body).get("fields"))).has(
3759
+ selection.reference,
3760
+ )
3761
+ )
3762
+ return members;
3763
+ return members.filter((id) =>
3764
+ resolveField(document, byId.get(id)!, `self.${selection.reference}`),
3765
+ );
3766
+ };
3767
+ // A count over no matchable member is zero.
3768
+ for (const inst of document.instruments)
3769
+ for (const action of Object.values(inst.actions))
3770
+ action.requires = action.requires.map((requirement) =>
3771
+ requirement.kind === "aggregate" &&
3772
+ requirement.measure === "count" &&
3773
+ !matchable(requirement.selection).length
3774
+ ? {
3775
+ kind: "compare",
3776
+ left: { literal: 0 },
3777
+ operator: requirement.operator,
3778
+ right: requirement.value,
3779
+ }
3780
+ : requirement,
3781
+ );
3719
3782
  const checkSelection = (
3720
3783
  selection: UdlSelection,
3721
3784
  at: Located,
@@ -3723,6 +3786,12 @@ export function compile(
3723
3786
  ) => {
3724
3787
  walk(selection.anchor, "anchor", site(selection, "anchor", at), scope);
3725
3788
  walk(selection.where, "where", site(selection, "where", at), scope);
3789
+ const members = matchable(selection);
3790
+ if (
3791
+ members.length &&
3792
+ members.length < [selection.instrument].flat().length
3793
+ )
3794
+ selection.instrument = members;
3726
3795
  for (const id of [selection.instrument].flat()) {
3727
3796
  checkTarget(id, site(selection, "instrument", at));
3728
3797
  const target = scopeOf(byId.get(id)!);