@hyperscale0/hsx 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/docs/headers.md CHANGED
@@ -3,42 +3,46 @@
3
3
  Each company selects library policies through typed tunables.
4
4
  `ref<T>[]` accepts one reference or up to 16 references as one policy. `approval` accepts a party and generates the required decision records.
5
5
 
6
- | Object | Tunables |
7
- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
8
- | money.transfer | `payer: party`, `payee: party`, `amount: money = runtime` |
9
- | money.hold | `payer: party`, `payee: party`, `amount: money = runtime` |
10
- | money.split | `payer: party`, `amount: money = runtime`, `shares: split = { programOperator: 100% }` |
11
- | money.schedule | `payer: party`, `payee: party`, `amount: money = runtime`, `count: integer(1, 366) = 12` |
12
- | money.pool | `payer: party`, `payee: party`, `target: money`, `closes: date` |
13
- | money.swap | `first: party`, `second: party`, `first_amount: money`, `second_amount: money`, `expires: date` |
14
- | money.deposit | `payer: party`, `payee: party`, `amount: money`, `expires: date`, `approval: approval` |
15
- | money.payout | `payer: party`, `payee: party`, `amount: money = runtime`, `reconcile_within: duration = 3d` |
16
- | money.metered | `payer: party`, `payee: party`, `unit_price: money`, `units: integer = 1`, `fee: fee = { seller: 0% }` |
17
- | money.batch | `payer: party`, `payee: party`, `closes: date`, `approval: approval`, `reconcile_within: duration = 3d` |
18
- | marketplace.listing | `seller: party` |
19
- | marketplace.order | `listing: ref<marketplace.listing>`, `buyer: party` |
20
- | escrow.hold | `payer: party`, `payee: party`, `for: ref<marketplace.order>`, `accept_within: duration = 48h`, `fee: fee = { seller: 0%, tax: 0% }`, `dispute: policy = { refund_after: return_verified }` |
21
- | wallet.balance | `holder: party`, `spend_limit: money = 100000 SAR` |
22
- | wallet.spend | `wallet: ref<wallet.balance>`, `payee: party` |
23
- | financing.installments | `months: integer(1, 366)`, `profit: percent`, `disburse_to: enum(funds, borrower) = funds`, `profit_earned: enum(on_payment, by_schedule, at_disbursement) = on_payment`, `apply: enum(fines_profit_principal, principal_profit, pro_rata) = fines_profit_principal`, `payoff_rebate: percent = 100%`, `down_payment: percent = 0%`, `funds: ref`, `approval: approval`, `borrower: party = party(person)`, `capital: party = programOperator`, `profit_to: party = programOperator`, `limits: ref<financing.limits> = object(financing.limits)` |
24
- | financing.late_charge | `on: ref<financing.installments>[]`, `grace: duration = 3d`, `fine: money = 50 SAR`, `cap: money = 25 SAR`, `fines_to: party = programOperator`, `costs_to: party = programOperator`, `approval: approval = programOperator`, `borrower: party = party(person)` |
25
- | financing.limits | `per_borrower: money`, `portfolio: money`, `borrower: party = party(person)`, `active_plans: integer = 1` |
26
- | financing.credit_line | `borrower: party`, `lender: party`, `limit: money`, `expires: date` |
27
- | financing.advance | `line: ref<financing.credit_line>`, `amount: money = runtime` |
28
- | lending.round | `borrower: party`, `capital: party`, `plan: ref<financing.installments> = object(financing.installments)`, `months: integer(1, 366) = 3`, `profit: percent = 3%`, `minimum_ticket: money = 100 SAR`, `investor_cap: percent = 20%`, `commitments: ref<lending.commitment> = object(lending.commitment)` |
29
- | lending.commitment | `round: ref<lending.round>`, `wallet: ref<wallet.balance>`, `investor: party = party(person)` |
30
- | lending.distribution | `round: ref<lending.round>`, `receipt: ref`, `commitments: ref<lending.commitment> = object(lending.commitment)`, `mode: enum(cash, loss) = cash`, `residual_to: party = programOperator`, `fee: percent = 1%`, `tax: percent = 15%` |
31
- | insurance.cover | `holder: party`, `insurer: party`, `commission: percent = 0%`, `covers: ref`, `premium: money = runtime` |
32
- | insurance.claim | `cover: ref<insurance.cover>`, `approved_by: approval` |
33
- | approvals.decision | `for: ref`, `approved_by: party`, `action: text = "approve"` |
34
- | collections.case | `on: ref<financing.installments>[]`, `agency: approval`, `overdue: duration = 3d`, `fee: percent = 20%` |
35
- | collections.contact | `case: ref<collections.case>[]`, `agency: party`, `max_contacts: integer = 10`, `window: duration = 30d`, `contact_from: integer(0, 23) = 8`, `contact_until: integer(1, 23) = 20`, `timezone: text = "Asia/Riyadh"`; contact_from less_than contact_until |
36
- | travel.package | `price: money`, `supplier_cost: money`, `departure: date`; supplier_cost at_most price |
37
- | travel.booking | `package: ref<travel.package>`, `buyer: party`, `supplier: approval`, `cover: ref<insurance.cover>`, `deposit: percent = 30%`, `balance_before: duration = 14d`, `confirm_within: duration = 24h`, `tax: percent = 15%`, `early_before: duration = 30d`, `middle_before: duration = 15d`, `middle_penalty: percent = 10%`, `late_penalty: percent = 30%`; early_before greater_than middle_before |
38
- | cards.cardholder | `holder: party` |
39
- | cards.card | `holder: ref<cards.cardholder>`, `spend_limit: money` |
40
- | cards.authorization | `card: ref<cards.card>`, `merchant: party`, `approval: approval` |
41
- | cards.transaction | `authorization: ref<cards.authorization>` |
42
- | cards.dispute | `transaction: ref<cards.transaction>`, `approved_by: approval`, `within: duration = 90d` |
43
- | savings.circle | `contribution: money`, `members: integer(1, 366)`, `starts: date`, `memberships: ref<savings.membership> = object(savings.membership)` |
44
- | savings.membership | `circle: ref<savings.circle>`, `member: party` |
6
+ | Object | Tunables |
7
+ | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
8
+ | money.transfer | `payer: party`, `payee: party`, `amount: money = runtime` |
9
+ | money.hold | `payer: party`, `payee: party`, `amount: money = runtime` |
10
+ | money.split | `payer: party`, `amount: money = runtime`, `shares: split = { programOperator: 100% }` |
11
+ | money.schedule | `payer: party`, `payee: party`, `amount: money = runtime`, `count: integer(1, 366) = 12` |
12
+ | money.pool | `payer: party`, `payee: party`, `target: money`, `closes: date` |
13
+ | money.swap | `first: party`, `second: party`, `first_amount: money`, `second_amount: money`, `expires: date` |
14
+ | money.deposit | `payer: party`, `payee: party`, `amount: money`, `expires: date`, `approval: approval` |
15
+ | money.payout | `payer: party`, `payee: party`, `amount: money = runtime`, `reconcile_within: duration = 3d` |
16
+ | money.metered | `payer: party`, `payee: party`, `unit_price: money`, `units: integer = 1`, `fee: fee = { seller: 0% }` |
17
+ | money.batch | `payer: party`, `payee: party`, `closes: date`, `approval: approval`, `reconcile_within: duration = 3d` |
18
+ | marketplace.listing | `seller: party`, `vehicle_facts: enum(optional, required) = optional` |
19
+ | marketplace.order | `listing: ref<marketplace.listing>`, `buyer: party` |
20
+ | marketplace.reservation | `listing: ref<marketplace.listing>`, `funds: ref<escrow.hold>`, `plans: ref<financing.installments>[]`, `converters: ref[]`, `buyer: party`, `seller: party`, `expires_after: duration = 48h`, `deposit: money = 1000 SAR`, `cancellation_refund: percent = 100%`, `expiry_refund: percent = 100%`, `retained_to: party = programOperator`, `operator: party = programOperator`, `competing_quotes: enum(allowed, blocked) = allowed` |
21
+ | escrow.hold | `payer: party`, `payee: party`, `for: ref<marketplace.order>`, `accept_within: duration = 48h`, `fee: fee = { seller: 0%, tax: 0% }`, `dispute: policy = { refund_after: return_verified }` |
22
+ | wallet.balance | `holder: party`, `spend_limit: money = 100000 SAR` |
23
+ | wallet.spend | `wallet: ref<wallet.balance>`, `payee: party` |
24
+ | financing.installments | `months: integer(1, 366)`, `charge_limit: integer(1, 3) = 3`, `profit: percent`, `disburse_to: enum(funds, borrower) = funds`, `profit_earned: enum(on_payment, by_schedule, at_disbursement) = on_payment`, `apply: enum(fines_profit_principal, principal_profit, pro_rata) = fines_profit_principal`, `payoff_rebate: percent = 100%`, `write_off_after: duration = 90d`, `max_extension: duration = 90d`, `max_amendments: integer(0, 366) = 2`, `waiver_limit: percent = 100%`, `amendment_expiry: duration = 7d`, `payoff_quote_expiry: duration = 1h`, `allow_overdue: enum(allowed, blocked) = allowed`, `assessed_fines: enum(carry, require_waiver) = carry`, `down_payment: percent = 0%`, `funds: ref`, `approval: approval`, `borrower: party = party(person)`, `capital: party = programOperator`, `profit_to: party = programOperator`, `limits: ref<financing.limits> = object(financing.limits)` |
25
+ | financing.late_charge | `on: ref<financing.installments>[]`, `grace: duration = 3d`, `fine: money = 50 SAR`, `cap: money = 25 SAR`, `fines_to: party = programOperator`, `costs_to: party = programOperator`, `approval: approval = programOperator`, `borrower: party = party(person)` |
26
+ | financing.limits | `per_borrower: money`, `portfolio: money`, `borrower: party = party(person)`, `active_plans: integer = 1` |
27
+ | financing.credit_line | `borrower: party`, `lender: party`, `limit: money`, `expires: date` |
28
+ | financing.advance | `line: ref<financing.credit_line>`, `amount: money = runtime` |
29
+ | lending.round | `borrower: party`, `capital: party`, `plan: ref<financing.installments> = object(financing.installments)`, `months: integer(1, 366) = 3`, `profit: percent = 3%`, `minimum_ticket: money = 100 SAR`, `investor_cap: percent = 20%`, `commitments: ref<lending.commitment> = object(lending.commitment)` |
30
+ | lending.commitment | `round: ref<lending.round>`, `wallet: ref<wallet.balance>`, `investor: party = party(person)` |
31
+ | lending.distribution | `round: ref<lending.round>`, `receipt: ref`, `commitments: ref<lending.commitment> = object(lending.commitment)`, `mode: enum(cash, loss) = cash`, `residual_to: party = programOperator`, `fee: percent = 1%`, `tax: percent = 15%` |
32
+ | insurance.cover | `holder: party`, `insurer: party`, `commission: percent = 0%`, `covers: ref`, `premium: money = runtime` |
33
+ | insurance.claim | `cover: ref<insurance.cover>`, `approved_by: approval` |
34
+ | approvals.decision | `for: ref`, `approved_by: party`, `action: text = "approve"` |
35
+ | collections.case | `on: ref<financing.installments>[]`, `agency: approval`, `overdue: duration = 3d`, `fee: percent = 20%` |
36
+ | collections.contact | `case: ref<collections.case>[]`, `agency: party`, `max_contacts: integer = 10`, `window: duration = 30d`, `contact_from: integer(0, 23) = 8`, `contact_until: integer(1, 23) = 20`, `timezone: text = "Asia/Riyadh"`; contact_from less_than contact_until |
37
+ | collections.reminder | `on: ref<financing.installments>[]`, `operator: party = programOperator`, `before_days: integer(0, 366) = 3`, `overdue_days: integer(1, 366) = 1`, `max_per_day: integer(1, 10) = 1`, `send_from: integer(0, 23) = 8`, `send_until: integer(1, 24) = 20`, `timezone: text = "Asia/Riyadh"`; send_from less_than send_until |
38
+ | travel.package | `price: money`, `supplier_cost: money`, `departure: date`; supplier_cost at_most price |
39
+ | travel.booking | `package: ref<travel.package>`, `buyer: party`, `supplier: approval`, `cover: ref<insurance.cover>`, `deposit: percent = 30%`, `balance_before: duration = 14d`, `confirm_within: duration = 24h`, `tax: percent = 15%`, `early_before: duration = 30d`, `middle_before: duration = 15d`, `middle_penalty: percent = 10%`, `late_penalty: percent = 30%`; early_before greater_than middle_before |
40
+ | cards.cardholder | `holder: party` |
41
+ | cards.card | `holder: ref<cards.cardholder>`, `spend_limit: money` |
42
+ | cards.authorization | `card: ref<cards.card>`, `merchant: party`, `approval: approval` |
43
+ | cards.transaction | `authorization: ref<cards.authorization>` |
44
+ | cards.dispute | `transaction: ref<cards.transaction>`, `approved_by: approval`, `within: duration = 90d` |
45
+ | savings.circle | `contribution: money`, `members: integer(1, 366)`, `starts: date`, `memberships: ref<savings.membership> = object(savings.membership)` |
46
+ | savings.membership | `circle: ref<savings.circle>`, `member: party` |
47
+ | reporting.portfolio | `on: ref<financing.installments>[]`, `default_days: integer(1, 3650) = 90`, `retention_years: integer(1, 100) = 10`, `aging_first_days: integer(1, 3650) = 30`, `aging_second_days: integer(1, 3650) = 60`, `aging_third_days: integer(1, 3650) = 90`, `ratio_scale: integer(1, 1000000) = 10000`, `ratio_rounding: enum(floor, halfUp) = floor`, `lock_wait_ms: integer(1, 2000) = 2000`, `capture_ms: integer(1, 10000) = 10000`, `max_rows: integer(1, 100000) = 10000`, `max_join_rows: integer(1, 1000000) = 100000`, `max_bytes: integer(1024, 16777216) = 8388608`; aging_first_days less_than aging_second_days, aging_second_days less_than aging_third_days |
48
+ | vehicles.vehicle | `listing: ref<marketplace.listing>`, `seller: party` |
@@ -20,7 +20,7 @@
20
20
  "verification.kyc.investment.count",
21
21
  "verification.kyc.standard.count"
22
22
  ],
23
- "effectiveTableDigest": "3cb4eb8a2524c1012da7db029f9f7a9cdb06052c78abd2144760497dc3ba0a2b",
23
+ "effectiveTableDigest": "92a2b90130954fe1ff685dde716b295ce0f5ad4ee9769a10aa04f3807fc64dc9",
24
24
  "fixed": {
25
25
  "activationBaseMinor": "500000",
26
26
  "activationPerPointMinor": "100000",
@@ -195,6 +195,14 @@
195
195
  "signature": "moves.transfer.internal",
196
196
  "volumeMeter": "transfer.internal.volume_sar"
197
197
  },
198
+ {
199
+ "bps": 0,
200
+ "meter": "instrument.event.count",
201
+ "payer": "product",
202
+ "perEventMinor": "0",
203
+ "settlement": "invoice",
204
+ "signature": "reminder.email"
205
+ },
198
206
  {
199
207
  "bps": 0,
200
208
  "meter": "instrument.event.count",
@@ -283,7 +291,7 @@
283
291
  "verification.kyc.investment.count",
284
292
  "verification.kyc.standard.count"
285
293
  ],
286
- "effectiveTableDigest": "70454efb5184bc9246896752e64e85eb0b616c41544469df083a2dc8014c0785",
294
+ "effectiveTableDigest": "b9b790c53a6f993c11b27dc06d9373ed5e865846343ab4893fd39ea2945dc520",
287
295
  "fixed": {
288
296
  "activationBaseMinor": "133300",
289
297
  "activationPerPointMinor": "26700",
@@ -458,6 +466,14 @@
458
466
  "signature": "moves.transfer.internal",
459
467
  "volumeMeter": "transfer.internal.volume_usd"
460
468
  },
469
+ {
470
+ "bps": 0,
471
+ "meter": "instrument.event.count",
472
+ "payer": "product",
473
+ "perEventMinor": "0",
474
+ "settlement": "invoice",
475
+ "signature": "reminder.email"
476
+ },
461
477
  {
462
478
  "bps": 0,
463
479
  "meter": "instrument.event.count",
@@ -546,7 +562,7 @@
546
562
  "verification.kyc.investment.count",
547
563
  "verification.kyc.standard.count"
548
564
  ],
549
- "effectiveTableDigest": "d782642a28ab9225c7967f865f73e9453a9683ca7b5b55c8df6e18a53bbdd8e7",
565
+ "effectiveTableDigest": "83ea8337ea1d8a9301156fec28df5ba941388cba1d83599b878f1f41d67d7af4",
550
566
  "fixed": {
551
567
  "activationBaseMinor": "125000",
552
568
  "activationPerPointMinor": "25000",
@@ -721,6 +737,14 @@
721
737
  "signature": "moves.transfer.internal",
722
738
  "volumeMeter": "transfer.internal.volume_eur"
723
739
  },
740
+ {
741
+ "bps": 0,
742
+ "meter": "instrument.event.count",
743
+ "payer": "product",
744
+ "perEventMinor": "0",
745
+ "settlement": "invoice",
746
+ "signature": "reminder.email"
747
+ },
724
748
  {
725
749
  "bps": 0,
726
750
  "meter": "instrument.event.count",
@@ -809,7 +833,7 @@
809
833
  "verification.kyc.investment.count",
810
834
  "verification.kyc.standard.count"
811
835
  ],
812
- "effectiveTableDigest": "0da12e42a0d68444e2776ef0905473698d2437e0d4eea5b7e5f5fad9e8d3fbfb",
836
+ "effectiveTableDigest": "5f24fe864b81b4d801353549381e7b17a73210ece0323db2328f0f794b554152",
813
837
  "fixed": {
814
838
  "activationBaseMinor": "104200",
815
839
  "activationPerPointMinor": "20800",
@@ -984,6 +1008,14 @@
984
1008
  "signature": "moves.transfer.internal",
985
1009
  "volumeMeter": "transfer.internal.volume_gbp"
986
1010
  },
1011
+ {
1012
+ "bps": 0,
1013
+ "meter": "instrument.event.count",
1014
+ "payer": "product",
1015
+ "perEventMinor": "0",
1016
+ "settlement": "invoice",
1017
+ "signature": "reminder.email"
1018
+ },
987
1019
  {
988
1020
  "bps": 0,
989
1021
  "meter": "instrument.event.count",
@@ -1,6 +1,7 @@
1
1
  program library "Standard library"
2
2
  use money
3
3
  use marketplace
4
+ use vehicles
4
5
  use escrow
5
6
  use wallet
6
7
  use financing
@@ -11,6 +12,7 @@ use collections
11
12
  use travel
12
13
  use cards
13
14
  use savings
15
+ use reporting
14
16
  party buyer: person
15
17
  party seller: business
16
18
  party officer: staff role credit_underwriter
@@ -27,7 +29,7 @@ deposit = money.deposit { payer: buyer, payee: seller, amount: 100 SAR, expires:
27
29
  payout = money.payout { payer: seller, payee: buyer, amount: 100 SAR, reconcile_within: 3d }
28
30
  metered = money.metered { payer: buyer, payee: seller, unit_price: 10 SAR, units: 2 }
29
31
  batch = money.batch { payer: buyer, payee: seller, closes: 2030-01-01, approval: officer }
30
- listing = marketplace.listing { seller: seller }
32
+ listing = marketplace.listing { seller: seller, vehicle_facts: required }
31
33
  order = marketplace.order { listing: listing, buyer: buyer }
32
34
  sale = escrow.hold { payer: buyer, payee: seller, for: order, fee { seller: 1% cap 750 SAR, tax: 15% }, dispute { refund_after: return_verified } }
33
35
  purse = wallet.balance { holder: buyer, spend_limit: 1000 SAR }
@@ -45,8 +47,9 @@ distribution = lending.distribution { round: round, receipt: plan.settlement, re
45
47
  cover = insurance.cover { holder: buyer, insurer: carrier, covers: sale, commission: 10% }
46
48
  claim = insurance.claim { cover: cover, approved_by: carrier }
47
49
  decision = approvals.decision { for: transfer, approved_by: officer, action: "pay" }
48
- case = collections.case { on: plan, agency: seller, fee: 20% }
50
+ case = collections.case { on: [plan, plan_schedule, plan_upfront], agency: seller, fee: 20% }
49
51
  contact = collections.contact { case: case, agency: seller }
52
+ reminder = collections.reminder { on: plan }
50
53
  package = travel.package { price: 1000 SAR, supplier_cost: 700 SAR, departure: 2030-01-01 }
51
54
  booking = travel.booking { package: package, buyer: buyer, supplier: seller, cover: travel_cover }
52
55
  cardholder = cards.cardholder { holder: buyer }
@@ -57,3 +60,7 @@ dispute = cards.dispute { transaction: transaction, approved_by: officer }
57
60
  circle = savings.circle { contribution: 100 SAR, members: 3, starts: 2030-01-01 }
58
61
  membership = savings.membership { circle: circle, member: buyer }
59
62
  travel_cover = insurance.cover { holder: buyer, insurer: carrier, commission: 10%, covers: booking }
63
+
64
+ reports = reporting.portfolio { on: [plan, plan_schedule, plan_upfront] }
65
+ vehicle = vehicles.vehicle { listing: listing, seller: seller }
66
+ reservation = marketplace.reservation { listing: listing, funds: sale, plans: [plan, plan_schedule, plan_upfront], converters: [sale, plan, plan_schedule, plan_upfront], buyer: buyer, seller: seller }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperscale0/hsx",
3
- "version": "3.1.0",
3
+ "version": "3.3.0",
4
4
  "description": "The strictly typed HSX language: compile total financial programs into canonical UDL.",
5
5
  "keywords": [
6
6
  "hsx",
@@ -76,7 +76,7 @@
76
76
  "check": "bun run generate && bun run typecheck && bun run build && tsc -p playground/tsconfig.json && bun scripts/build-playground.ts && bun test test"
77
77
  },
78
78
  "dependencies": {
79
- "@hyperscale0/udl": "3.1.0"
79
+ "@hyperscale0/udl": "3.2.0"
80
80
  },
81
81
  "devDependencies": {
82
82
  "bun-types": "1.4.2",
@@ -87,5 +87,5 @@
87
87
  "Amir Ayub",
88
88
  "Sara AlBakaawi"
89
89
  ],
90
- "gitHead": "a65150e9cc5a0d5506817f0feb9d64ff62b1d964"
90
+ "gitHead": "6f9f3c0315ec07edeafca09596d3cf92c53a25fd"
91
91
  }
package/src/compile.ts CHANGED
@@ -596,6 +596,8 @@ export function compile(
596
596
  "summary",
597
597
  "invariants",
598
598
  "constraints",
599
+ "reports",
600
+ "revisioned",
599
601
  ].includes(key) &&
600
602
  !key.startsWith("action ")
601
603
  )
@@ -698,6 +700,17 @@ export function compile(
698
700
  ...(t.kind === "type" && t.optional ? { optional: true } : {}),
699
701
  };
700
702
  if (type === "enum" && t.kind === "call") f.values = t.args.map(text);
703
+ if (type === "text" && t.kind === "call") {
704
+ if (t.args.length < 2 || t.args.length > 3)
705
+ fail(
706
+ t,
707
+ "bounded text needs length bounds and an optional pattern",
708
+ "write text(1, 80)",
709
+ );
710
+ f.minLength = literal(resolve(t.args[0]!));
711
+ f.maxLength = literal(resolve(t.args[1]!));
712
+ if (t.args[2]) f.pattern = literal(resolve(t.args[2]));
713
+ }
701
714
  if (["integer", "money"].includes(type) && t.kind === "call") {
702
715
  if (t.args.length !== 2)
703
716
  fail(
@@ -894,20 +907,53 @@ export function compile(
894
907
  ...block,
895
908
  entries: block.entries.flatMap((entry) => {
896
909
  if (!entry.key.startsWith("when ")) return [entry];
897
- const [, tunable, , choice] = entry.key.split(" ");
910
+ const [, tunable, relation, choice] = entry.key.split(" ");
898
911
  const binding = environment.get(tunable!);
899
912
  if (!binding)
900
913
  fail(
901
914
  entry,
902
915
  `unknown branch tunable ${tunable}`,
903
- "name an enum tunable declared by this header",
916
+ "name an enum or reference tunable declared by this header",
904
917
  );
905
- if (!enums.get(tunable!)?.includes(choice!))
906
- fail(
907
- entry,
908
- `unknown enum branch ${choice}`,
909
- "use a declared enum value",
918
+ let matches: boolean;
919
+ if (relation === "has") {
920
+ // Inspect declarations, not lowering order: a bound object may
921
+ // appear after the instrument that asks about its fields.
922
+ const bound = resolve(binding!);
923
+ const [root, ...children] = text(bound).split(".");
924
+ const object = objects.get(root!);
925
+ let target = object
926
+ ? templates.get(object.object)?.body
927
+ : program.decls
928
+ .filter(
929
+ (decl): decl is InstrumentDecl =>
930
+ decl.kind === "instrument",
931
+ )
932
+ .find((decl) => decl.name === root)?.body;
933
+ for (const child of children) {
934
+ const record = target
935
+ ? entries(asBlock(entries(target).get("records"))).get(child)
936
+ : undefined;
937
+ target = record ? asBlock(record) : undefined;
938
+ }
939
+ if (!target)
940
+ fail(
941
+ entry,
942
+ "field branch needs a declared object",
943
+ "bind a reference to a declared object",
944
+ );
945
+ matches = entries(asBlock(entries(target!).get("fields"))).has(
946
+ choice!,
910
947
  );
948
+ } else {
949
+ if (!enums.get(tunable!)?.includes(choice!))
950
+ fail(
951
+ entry,
952
+ `unknown enum branch ${choice}`,
953
+ "use a declared enum value",
954
+ );
955
+ matches = text(binding!) === choice;
956
+ }
911
957
  const body = asBlock(entry.value);
912
958
  for (const clause of body.entries)
913
959
  if (
@@ -921,7 +967,7 @@ export function compile(
921
967
  "when permits requirements, calculations, moves and invocations",
922
968
  "keep lifecycle and actor clauses outside the branch",
923
969
  );
924
- return text(binding) === choice ? selected(body).entries : [];
970
+ return matches ? selected(body).entries : [];
925
971
  }),
926
972
  });
927
973
  const slots = entries(selected(asBlock(row.value)));
@@ -1278,7 +1324,7 @@ export function compile(
1278
1324
  inst.actions[name] = a;
1279
1325
  inst.actionOrder.push(name);
1280
1326
  }
1281
- for (const key of ["invariants"] as const)
1327
+ for (const key of ["invariants", "reports", "revisioned"] as const)
1282
1328
  if (body.has(key))
1283
1329
  (inst as unknown as Record<string, unknown>)[key] = data(
1284
1330
  body.get(key)!,
@@ -1361,7 +1407,7 @@ export function compile(
1361
1407
  for (let n = 0; n < inst.lifecycle.states.length; n++)
1362
1408
  for (const edge of Object.values(inst.lifecycle.transitions))
1363
1409
  if (edge.from.some((state) => reachable.has(state)))
1364
- reachable.add(edge.to);
1410
+ if (edge.to !== "preserve") reachable.add(edge.to);
1365
1411
  for (const [key, edge] of Object.entries(inst.lifecycle.transitions)) {
1366
1412
  edge.from = edge.from.filter((state) => reachable.has(state));
1367
1413
  if (!edge.from.length) {
package/src/cost.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { UdlDocument } from "@hyperscale0/udl";
1
+ import { resolveField, type UdlDocument } from "@hyperscale0/udl";
2
2
  export interface UdlCostManifest {
3
3
  actions: Record<
4
4
  string,
@@ -12,21 +12,65 @@ export interface UdlCostManifest {
12
12
  /** Counts sealed instructions after lowering. Prices belong to the executor's tariff. */
13
13
  export function buildUdlCostManifest(document: UdlDocument): UdlCostManifest {
14
14
  const actions: UdlCostManifest["actions"] = {};
15
- for (const instrument of document.instruments)
16
- for (const [name, action] of Object.entries(instrument.actions)) {
17
- actions[`${instrument.id}.${name}`] = {
18
- transfers: action.moves.length,
19
- accounts:
20
- name === "create"
21
- ? instrument.fields.filter(
22
- (f) => f.type === "account" && f.owner === "self",
23
- ).length
24
- : 0,
25
- invocations: (action.invoke ?? []).reduce(
26
- (sum, call) => sum + ("selection" in call ? call.selection.limit : 1),
27
- 0,
28
- ),
29
- };
15
+ const active = new Set<string>();
16
+ const visit = (
17
+ id: string,
18
+ name: string,
19
+ ): UdlCostManifest["actions"][string] => {
20
+ const key = `${id}.${name}`;
21
+ if (actions[key]) return actions[key];
22
+ if (active.has(key)) throw new Error(`Invocation cycle at ${key}`);
23
+ const instrument = document.instruments.find((item) => item.id === id);
24
+ const action = instrument?.actions[name];
25
+ if (!instrument || !action) throw new Error(`Unknown invocation ${key}`);
26
+ active.add(key);
27
+ const cost = {
28
+ transfers: action.moves.length,
29
+ accounts:
30
+ name === "create"
31
+ ? instrument.fields.filter(
32
+ (field) => field.type === "account" && field.owner === "self",
33
+ ).length
34
+ : 0,
35
+ invocations: 0,
36
+ };
37
+ for (const call of action.invoke ?? []) {
38
+ const ref =
39
+ "reference" in call
40
+ ? resolveField(document, instrument, call.reference, action.input)
41
+ : undefined;
42
+ const targets =
43
+ "instrument" in call
44
+ ? call.instrument
45
+ : "selection" in call
46
+ ? call.selection.instrument
47
+ : ref?.type === "ref"
48
+ ? ref.target
49
+ : [];
50
+ const children = (typeof targets === "string" ? [targets] : targets).map(
51
+ (target) => visit(target, call.action),
52
+ );
53
+ const count =
54
+ "selection" in call
55
+ ? call.selection.limit
56
+ : "instrument" in call
57
+ ? (call.range?.maximum ?? 1)
58
+ : 1;
59
+ // A union selects one target per row. Each meter takes its worst case.
60
+ cost.transfers +=
61
+ count * Math.max(0, ...children.map((child) => child.transfers));
62
+ cost.accounts +=
63
+ count * Math.max(0, ...children.map((child) => child.accounts));
64
+ cost.invocations +=
65
+ count *
66
+ (1 + Math.max(0, ...children.map((child) => child.invocations)));
30
67
  }
68
+ active.delete(key);
69
+ actions[key] = cost;
70
+ return cost;
71
+ };
72
+ for (const instrument of document.instruments)
73
+ for (const name of Object.keys(instrument.actions))
74
+ visit(instrument.id, name);
31
75
  return { actions };
32
76
  }
package/src/headers.ts CHANGED
@@ -16,6 +16,8 @@ export const HEADER_NAMES = [
16
16
  "travel",
17
17
  "cards",
18
18
  "savings",
19
+ "reporting",
20
+ "vehicles",
19
21
  ] as const;
20
22
 
21
23
  /** The compiler frontend owns header metadata used by docs and catalogue consumers. */
@@ -84,6 +86,18 @@ export function headerManifest(
84
86
  ...(fallback ? { default: spelling(fallback) } : {}),
85
87
  };
86
88
  });
89
+ // Reference selectors need program context, and text can name a target
90
+ // action. Templates require explicit bindings instead of guessing them.
91
+ const bindings = tunables.filter((tunable, index) => {
92
+ const value = decl.parameters[index]!.value;
93
+ return (
94
+ tunable.required ||
95
+ tunable.type === "text" ||
96
+ (value.kind === "default" &&
97
+ value.value.kind === "call" &&
98
+ ["object", "party"].includes(value.value.name))
99
+ );
100
+ });
87
101
  const summary = decl.body.entries.find(
88
102
  (e) => e.key === "summary",
89
103
  )?.value;
@@ -94,6 +108,22 @@ export function headerManifest(
94
108
  summary?.kind === "text"
95
109
  ? summary.value
96
110
  : decl.name.replaceAll("_", " "),
111
+ authoringTemplate: {
112
+ requiredImport: `use ${name}`,
113
+ instancePlaceholder: "${instance}",
114
+ source:
115
+ "${instance} = " +
116
+ `${name}.${decl.name} { ` +
117
+ bindings
118
+ .map((t) => t.name + ": ${" + t.name + "}")
119
+ .join(", ") +
120
+ " }",
121
+ requiredBindings: bindings.map((t) => ({
122
+ name: t.name,
123
+ type: t.type,
124
+ placeholder: "${" + t.name + "}",
125
+ })),
126
+ },
97
127
  tunables,
98
128
  constraints: (() => {
99
129
  const block = decl.body.entries.find(