@hyperscale0/hsx 1.0.342 → 1.0.347

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 (128) hide show
  1. package/README.md +1 -1
  2. package/bases/cards.hsx +0 -1
  3. package/bases/collections.hsx +0 -2
  4. package/bases/escrow.hsx +0 -1
  5. package/bases/financing.hsx +0 -1
  6. package/bases/insurance.hsx +0 -1
  7. package/bases/lending.hsx +0 -3
  8. package/bases/marketplace.hsx +0 -2
  9. package/bases/money.hsx +0 -1
  10. package/bases/reporting.hsx +0 -2
  11. package/bases/savings.hsx +0 -1
  12. package/bases/wallet.hsx +0 -1
  13. package/dist/src/ast.d.ts +57 -7
  14. package/dist/src/ast.d.ts.map +1 -1
  15. package/dist/src/ast.js.map +1 -1
  16. package/dist/src/bases-bundle.js +11 -11
  17. package/dist/src/bases-bundle.js.map +1 -1
  18. package/dist/src/catalog-program.d.ts +1 -1
  19. package/dist/src/catalog-program.d.ts.map +1 -1
  20. package/dist/src/catalog-program.js +0 -13
  21. package/dist/src/catalog-program.js.map +1 -1
  22. package/dist/src/checks.d.ts +60 -0
  23. package/dist/src/checks.d.ts.map +1 -0
  24. package/dist/src/checks.js +240 -0
  25. package/dist/src/checks.js.map +1 -0
  26. package/dist/src/cli.d.ts.map +1 -1
  27. package/dist/src/cli.js +6 -25
  28. package/dist/src/cli.js.map +1 -1
  29. package/dist/src/compile.d.ts +29 -12
  30. package/dist/src/compile.d.ts.map +1 -1
  31. package/dist/src/compile.js +414 -316
  32. package/dist/src/compile.js.map +1 -1
  33. package/dist/src/diagnostics.d.ts +14 -1
  34. package/dist/src/diagnostics.d.ts.map +1 -1
  35. package/dist/src/diagnostics.js +31 -0
  36. package/dist/src/diagnostics.js.map +1 -1
  37. package/dist/src/examples-bundle.js +15 -15
  38. package/dist/src/examples-bundle.js.map +1 -1
  39. package/dist/src/header-source.d.ts +30 -0
  40. package/dist/src/header-source.d.ts.map +1 -1
  41. package/dist/src/header-source.js +120 -9
  42. package/dist/src/header-source.js.map +1 -1
  43. package/dist/src/headers.d.ts +9 -18
  44. package/dist/src/headers.d.ts.map +1 -1
  45. package/dist/src/headers.js +25 -23
  46. package/dist/src/headers.js.map +1 -1
  47. package/dist/src/index.d.ts +4 -3
  48. package/dist/src/index.d.ts.map +1 -1
  49. package/dist/src/index.js +2 -2
  50. package/dist/src/index.js.map +1 -1
  51. package/dist/src/keywords.d.ts +2 -4
  52. package/dist/src/keywords.d.ts.map +1 -1
  53. package/dist/src/keywords.js +6 -5
  54. package/dist/src/keywords.js.map +1 -1
  55. package/dist/src/language.d.ts +3 -4
  56. package/dist/src/language.d.ts.map +1 -1
  57. package/dist/src/language.js +123 -60
  58. package/dist/src/language.js.map +1 -1
  59. package/dist/src/lex.d.ts +1 -1
  60. package/dist/src/lex.d.ts.map +1 -1
  61. package/dist/src/lex.js +3 -5
  62. package/dist/src/lex.js.map +1 -1
  63. package/dist/src/parse.d.ts +3 -1
  64. package/dist/src/parse.d.ts.map +1 -1
  65. package/dist/src/parse.js +234 -22
  66. package/dist/src/parse.js.map +1 -1
  67. package/dist/src/std-bundle.d.ts +2 -1
  68. package/dist/src/std-bundle.d.ts.map +1 -1
  69. package/dist/src/std-bundle.js +16 -15
  70. package/dist/src/std-bundle.js.map +1 -1
  71. package/dist/src/template-guide.d.ts +3 -9
  72. package/dist/src/template-guide.d.ts.map +1 -1
  73. package/dist/src/template-guide.js +16 -23
  74. package/dist/src/template-guide.js.map +1 -1
  75. package/dist/src/toolchain.d.ts +23 -0
  76. package/dist/src/toolchain.d.ts.map +1 -0
  77. package/dist/src/toolchain.js +29 -0
  78. package/dist/src/toolchain.js.map +1 -0
  79. package/docs/README.md +69 -28
  80. package/examples/camera-rental.hsx +0 -1
  81. package/examples/cards.hsx +0 -1
  82. package/examples/collections.hsx +0 -2
  83. package/examples/escrow.hsx +0 -1
  84. package/examples/financing.hsx +0 -1
  85. package/examples/insurance.hsx +0 -2
  86. package/examples/lending.hsx +0 -3
  87. package/examples/library.hsx +0 -3
  88. package/examples/marketplace.hsx +0 -2
  89. package/examples/money.hsx +0 -1
  90. package/examples/rental-hire.hsx +0 -1
  91. package/examples/reporting.hsx +0 -2
  92. package/examples/savings.hsx +0 -1
  93. package/examples/serviced.hsx +0 -4
  94. package/examples/wallet.hsx +0 -1
  95. package/package.json +4 -4
  96. package/src/ast.ts +56 -3
  97. package/src/bases-bundle.ts +11 -11
  98. package/src/catalog-program.ts +0 -13
  99. package/src/checks.ts +386 -0
  100. package/src/cli.ts +6 -25
  101. package/src/compile.ts +583 -438
  102. package/src/diagnostics.ts +49 -1
  103. package/src/examples-bundle.ts +15 -15
  104. package/src/header-source.ts +187 -11
  105. package/src/headers.ts +31 -32
  106. package/src/index.ts +10 -4
  107. package/src/keywords.ts +6 -5
  108. package/src/language.ts +153 -68
  109. package/src/lex.ts +3 -5
  110. package/src/parse.ts +216 -24
  111. package/src/std-bundle.ts +17 -15
  112. package/src/template-guide.ts +17 -34
  113. package/src/toolchain.ts +51 -0
  114. package/std/booking.hsx +14 -1
  115. package/std/collections.hsx +12 -2
  116. package/std/financing.hsx +10 -16
  117. package/std/money.hsx +2 -3
  118. package/std/reporting.hsx +10 -1
  119. package/dist/src/binding-contract.d.ts +0 -22
  120. package/dist/src/binding-contract.d.ts.map +0 -1
  121. package/dist/src/binding-contract.js +0 -80
  122. package/dist/src/binding-contract.js.map +0 -1
  123. package/dist/src/std-library.d.ts +0 -5
  124. package/dist/src/std-library.d.ts.map +0 -1
  125. package/dist/src/std-library.js +0 -5
  126. package/dist/src/std-library.js.map +0 -1
  127. package/src/binding-contract.ts +0 -111
  128. package/src/std-library.ts +0 -7
@@ -1,7 +1,6 @@
1
1
  // Seattle shopkeepers circle collects 500 SAR from each of six members every month.
2
2
  // Each member receives one 3,000 SAR pot in order; missed contributions remain payable and an unfunded pot waits.
3
3
  program neighbourhood_savings "Seattle shopkeepers circle"
4
- use savings
5
4
 
6
5
  // Fee: 0%. Tax: 0% because this circle only returns members' principal and
7
6
  // charges no service. The organiser keeps none of the 18,000 SAR contributed.
@@ -1,10 +1,6 @@
1
1
  // Denver delivery van finance funds a 69,000 SAR van with 10% down and 36 monthly instalments.
2
2
  // It records reminders, late charges, hardship extensions, early repayment and financed returns.
3
3
  program car_financing "Denver delivery van finance"
4
- use escrow
5
- use financing
6
- use purchase
7
- use collections
8
4
  party dealer: business
9
5
 
10
6
  // The dealer invoices 60,000 SAR plus 15% VAT. Set price to 69,000 SAR.
@@ -1,7 +1,6 @@
1
1
  // York pottery studio lets members prepay for visits at 172.50 SAR each.
2
2
  // A reserved visit can be charged, cancelled or released at expiry; unused credit can be withdrawn.
3
3
  program workshop_credits "York pottery studio"
4
- use wallet
5
4
 
6
5
  // Top-up fee: 0%. Loading and withdrawing credit have tax 0% here because
7
6
  // they do not sell a visit. Each visit is 150 SAR plus 15% VAT, invoiced by
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperscale0/hsx",
3
- "version": "1.0.342",
3
+ "version": "1.0.347",
4
4
  "description": "The strictly typed HSX language: compile total financial programs into canonical UDL.",
5
5
  "keywords": [
6
6
  "hsx",
@@ -90,8 +90,8 @@
90
90
  "bases:bundle": "bun scripts/bundle-bases.ts"
91
91
  },
92
92
  "dependencies": {
93
- "@hyperscale0/adl": "1.0.342",
94
- "@hyperscale0/udl": "1.0.342",
93
+ "@hyperscale0/adl": "1.0.347",
94
+ "@hyperscale0/udl": "1.0.347",
95
95
  "fast-sha256": "1.3.0"
96
96
  },
97
97
  "devDependencies": {
@@ -103,5 +103,5 @@
103
103
  "Amir Ayub",
104
104
  "Sara AlBakaawi"
105
105
  ],
106
- "gitHead": "e17fb66eb7dc3ff851b93934665225d065b02cfb"
106
+ "gitHead": "010c1b8d88b7d61ec9f65a6062fcfc65ea3dd976"
107
107
  }
package/src/ast.ts CHANGED
@@ -80,15 +80,68 @@ export type Decl =
80
80
  role?: string;
81
81
  span: Span;
82
82
  }
83
- | { kind: "use"; name: string; span: Span }
84
- | { kind: "expose"; target: string; name: string; span: Span }
85
- | { kind: "hide"; target: string; span: Span };
83
+ | { kind: "expose"; target: string; name: string; span: Span };
84
+ /** A name as written, so a check diagnostic points at it. */
85
+ export interface Named {
86
+ name: string;
87
+ span: Span;
88
+ }
89
+ /** One statement of a `check` block. Every step may end with `balances { }`. */
90
+ export type CheckStatement = { span: Span; balances?: BlockExpr } & (
91
+ | {
92
+ kind: "customer";
93
+ id: Named;
94
+ title: string;
95
+ organization: boolean;
96
+ fund?: Expr;
97
+ binds: Named[];
98
+ }
99
+ | {
100
+ kind: "clock";
101
+ at: Expr;
102
+ until?: {
103
+ object: Named;
104
+ attachment?: Named;
105
+ recordPath?: string;
106
+ state: Named;
107
+ };
108
+ }
109
+ | {
110
+ kind: "object";
111
+ id: Named;
112
+ objectKind: Named;
113
+ owner: Named;
114
+ fields?: BlockExpr;
115
+ }
116
+ | {
117
+ kind: "action";
118
+ actor?: Named;
119
+ object: Named;
120
+ action: Named;
121
+ input?: BlockExpr;
122
+ fields?: BlockExpr;
123
+ instance?: Named;
124
+ remember?: Named;
125
+ state?: Named;
126
+ status?: Expr;
127
+ code?: string;
128
+ }
129
+ | { kind: "close"; customer: Named }
130
+ | { kind: "expect"; held?: Expr; pending?: BlockExpr }
131
+ );
132
+ export interface CheckDecl {
133
+ name: string;
134
+ statements: CheckStatement[];
135
+ span: Span;
136
+ }
86
137
  export interface Program {
87
138
  name: string;
88
139
  title: string;
89
140
  currency: string;
90
141
  header: boolean;
91
142
  decls: Decl[];
143
+ /** Build data beside the document; compile never lowers them into it. */
144
+ checks: CheckDecl[];
92
145
  span: Span;
93
146
  }
94
147
  export const lineColAt = (source: string, offset: number) => {
@@ -1,15 +1,15 @@
1
1
  // Generated by open/hsx/scripts/bundle-bases.ts. Do not edit by hand.
2
2
  import type { HsxBase } from "./bases.ts";
3
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 }\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 }\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"},
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\"\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":["financing","collections"],"source":"// Refer overdue instalments for collection and limit contact attempts.\nprogram collections_base \"Follow up overdue payments\"\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\"\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","purchase"],"source":"// Fund a purchase and collect its scheduled repayments.\nprogram financing_base \"Split a purchase into instalments\"\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","purchase"],"source":"// Collect cover premiums and reserve approved claims for payment.\nprogram insurance_base \"Protect a purchase\"\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":["financing","wallet","lending"],"source":"// Pool member commitments to fund a loan and distribute repayments or losses.\nprogram lending_base \"Pool funds for lending\"\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 }\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\"\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\"\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":["financing","reporting","purchase"],"source":"// Report outstanding balances and overdue repayments on financed purchases.\nprogram reporting_base \"Track repayments\"\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\"\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\"\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 }\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
15
  ];
@@ -4,19 +4,6 @@
4
4
  * std-headers.spec proves the whole program compiles.
5
5
  */
6
6
  export const catalogProgramSource = `program catalog "Template catalog"
7
- use money
8
- use escrow
9
- use purchase
10
- use booking
11
- use marketplace
12
- use financing
13
- use insurance
14
- use lending
15
- use wallet
16
- use cards
17
- use collections
18
- use savings
19
- use reporting
20
7
  party supplier: business
21
8
  party inspector: staff role claim_inspector
22
9
  party investor: business
package/src/checks.ts ADDED
@@ -0,0 +1,386 @@
1
+ import type { UdlDocument } from "@hyperscale0/udl";
2
+ import type { BlockExpr, CheckDecl, Expr, Named } from "./ast.ts";
3
+ import { didYouMean, fail } from "./diagnostics.ts";
4
+
5
+ type Balances = Record<string, string>;
6
+ type Values = Record<string, unknown>;
7
+ type Expected = { expected?: Balances };
8
+ export type CompiledCheckStep = Expected &
9
+ (
10
+ | {
11
+ kind: "clock";
12
+ at: string;
13
+ waitFor?: {
14
+ object: string;
15
+ state: string;
16
+ attachment?: string;
17
+ recordPath?: string;
18
+ };
19
+ }
20
+ | {
21
+ kind: "object";
22
+ id: string;
23
+ objectKind: string;
24
+ owner: string;
25
+ fields?: Values;
26
+ }
27
+ | {
28
+ kind: "action";
29
+ object: string;
30
+ action: string;
31
+ actor?: string;
32
+ input?: Values;
33
+ fields?: Values;
34
+ state?: string;
35
+ status?: number | "absent";
36
+ code?: string;
37
+ remember?: string;
38
+ instance?: string;
39
+ }
40
+ | { kind: "close"; customer: string }
41
+ );
42
+ /** A check as the Build carries it; `buildCheckSchema` in contracts parses it. */
43
+ export interface CompiledCheck {
44
+ name: string;
45
+ customers: {
46
+ id: string;
47
+ name: string;
48
+ kind?: "organization";
49
+ fund?: string;
50
+ }[];
51
+ bindings?: Record<string, string>;
52
+ steps: CompiledCheckStep[];
53
+ expected: Balances;
54
+ expectedHeld?: string;
55
+ pending?: Record<string, Balances>;
56
+ }
57
+
58
+ // Statement keywords cannot name a customer, since a step opens with its actor.
59
+ const reserved = new Set(["customer", "clock", "object", "close", "expect"]);
60
+ /** The Product pool's balance name. */
61
+ const operator = "operator";
62
+
63
+ /**
64
+ * Lowers every check against the compiled document. A name the document does
65
+ * not declare fails at the step that wrote it.
66
+ */
67
+ export function lowerChecks(
68
+ checks: readonly CheckDecl[],
69
+ document: UdlDocument,
70
+ literal: (expr: Expr) => string | number | boolean,
71
+ ): CompiledCheck[] {
72
+ const seen = new Set<string>();
73
+ return checks.map((check) => {
74
+ if (seen.has(check.name))
75
+ fail(check, `check "${check.name}" is declared twice`, "rename one");
76
+ seen.add(check.name);
77
+ return lowerCheck(check, document, literal);
78
+ });
79
+ }
80
+
81
+ function lowerCheck(
82
+ check: CheckDecl,
83
+ document: UdlDocument,
84
+ literal: (expr: Expr) => string | number | boolean,
85
+ ): CompiledCheck {
86
+ const customers: CompiledCheck["customers"] = [];
87
+ const bindings: Record<string, string> = {};
88
+ const steps: CompiledCheckStep[] = [];
89
+ const objects = new Map<string, string>();
90
+ const remembered = new Set<string>();
91
+ let final: Pick<CompiledCheck, "expected" | "expectedHeld" | "pending"> =
92
+ undefined!;
93
+ const known = (
94
+ named: Named,
95
+ names: Iterable<string>,
96
+ what: string,
97
+ fix: string,
98
+ ) => {
99
+ const list = [...names];
100
+ if (!list.includes(named.name))
101
+ fail(
102
+ named,
103
+ `${what} ${named.name} is not declared.${didYouMean(named.name, list)}`,
104
+ fix,
105
+ );
106
+ return named.name;
107
+ };
108
+ const customer = (named: Named) =>
109
+ known(
110
+ named,
111
+ customers.map((c) => c.id),
112
+ "customer",
113
+ `declare customer ${named.name} "Name" before this step`,
114
+ );
115
+ const money = (expr: Expr) => {
116
+ if (expr.kind !== "money")
117
+ fail(expr, "expected an amount", "write an amount such as 100 SAR");
118
+ return literal(expr) as string;
119
+ };
120
+ const balanceNames = () => [operator, ...customers.map((c) => c.id)];
121
+ const balanceFix = `name a customer or ${operator}`;
122
+ const balances = (block: BlockExpr) =>
123
+ Object.fromEntries(
124
+ block.entries.map((entry) => [
125
+ known(
126
+ { name: entry.key, span: entry.span },
127
+ balanceNames(),
128
+ "balance",
129
+ balanceFix,
130
+ ),
131
+ money(entry.value),
132
+ ]),
133
+ );
134
+ const value = (expr: Expr): unknown => {
135
+ if (expr.kind === "list") return expr.items.map(value);
136
+ if (expr.kind === "block")
137
+ return Object.fromEntries(
138
+ expr.entries.map((entry) => [entry.key, value(entry.value)]),
139
+ );
140
+ if (expr.kind === "call" && ["agreement", "object"].includes(expr.name)) {
141
+ const [arg] = expr.args;
142
+ if (expr.args.length !== 1 || arg?.kind !== "name")
143
+ fail(expr, `${expr.name} takes one name`, `write ${expr.name}(name)`);
144
+ const name = { name: arg.value, span: arg.span };
145
+ if (expr.name === "agreement")
146
+ known(name, remembered, "agreement", "remember it on an earlier step");
147
+ else
148
+ known(name, objects.keys(), "object", "create it on an earlier step");
149
+ return `$${expr.name}:${arg.value}`;
150
+ }
151
+ return literal(expr);
152
+ };
153
+ const values = (block: BlockExpr) => value(block) as Values;
154
+ const instruments = (object: string) => {
155
+ const declared = document.objects.find((o) => o.id === object)!;
156
+ const ids = new Set(declared.attachments.map((a) => a.instrument));
157
+ return document.instruments.filter((i) => ids.has(i.id));
158
+ };
159
+ for (const statement of check.statements) {
160
+ if (final)
161
+ fail(statement, "expect closes a check", "move this step before expect");
162
+ const expected = statement.balances
163
+ ? { expected: balances(statement.balances) }
164
+ : {};
165
+ switch (statement.kind) {
166
+ case "customer": {
167
+ const { id } = statement;
168
+ if (reserved.has(id.name) || id.name === operator)
169
+ fail(id, `${id.name} cannot name a customer`, "pick another name");
170
+ if (customers.some((c) => c.id === id.name))
171
+ fail(id, `customer ${id.name} is declared twice`, "rename one");
172
+ if (steps.length)
173
+ fail(statement, "customers come before steps", "move it up");
174
+ customers.push({
175
+ id: id.name,
176
+ name: statement.title,
177
+ ...(statement.organization ? { kind: "organization" as const } : {}),
178
+ ...(statement.fund ? { fund: money(statement.fund) } : {}),
179
+ });
180
+ for (const party of statement.binds) {
181
+ const declared = Object.keys(document.parties);
182
+ known(party, declared, "party", "bind a party the program declares");
183
+ if (party.name in bindings)
184
+ fail(party, `party ${party.name} is bound twice`, "bind it once");
185
+ bindings[party.name] = id.name;
186
+ }
187
+ break;
188
+ }
189
+ case "clock": {
190
+ const at = statement.at;
191
+ if (at.kind !== "date")
192
+ fail(at, "a clock step needs a date", "write clock 2031-04-01");
193
+ const until = statement.until;
194
+ let waitFor: Extract<CompiledCheckStep, { kind: "clock" }>["waitFor"];
195
+ if (until) {
196
+ const object = known(
197
+ until.object,
198
+ objects.keys(),
199
+ "object",
200
+ "create it on an earlier step",
201
+ );
202
+ const kind = objects.get(object)!;
203
+ const declared = document.objects.find((o) => o.id === kind)!;
204
+ if (until.attachment)
205
+ known(
206
+ until.attachment,
207
+ declared.attachments.map((a) => a.name),
208
+ `${kind} attachment`,
209
+ `name an attachment of object ${kind}`,
210
+ );
211
+ known(
212
+ until.state,
213
+ instruments(kind).flatMap((i) => i.lifecycle.states),
214
+ "state",
215
+ `name a lifecycle state on object ${kind}`,
216
+ );
217
+ waitFor = {
218
+ object,
219
+ ...(until.recordPath ? { recordPath: until.recordPath } : {}),
220
+ state: until.state.name,
221
+ ...(until.attachment ? { attachment: until.attachment.name } : {}),
222
+ };
223
+ }
224
+ steps.push({
225
+ kind: "clock",
226
+ at: literal(at) as string,
227
+ ...(waitFor ? { waitFor } : {}),
228
+ ...expected,
229
+ });
230
+ break;
231
+ }
232
+ case "object": {
233
+ const { id } = statement;
234
+ if (objects.has(id.name))
235
+ fail(id, `object ${id.name} is created twice`, "rename one");
236
+ const kind = known(
237
+ statement.objectKind,
238
+ document.objects.map((o) => o.id),
239
+ "object kind",
240
+ "name an object the program declares",
241
+ );
242
+ const owner = customer(statement.owner);
243
+ const fieldNames = document.objects
244
+ .find((o) => o.id === kind)!
245
+ .fields.map((f) => f.name);
246
+ for (const entry of statement.fields?.entries ?? [])
247
+ known(
248
+ { name: entry.key, span: entry.span },
249
+ fieldNames,
250
+ `${kind} field`,
251
+ `name a field of object ${kind}`,
252
+ );
253
+ objects.set(id.name, kind);
254
+ steps.push({
255
+ kind: "object",
256
+ id: id.name,
257
+ objectKind: kind,
258
+ owner,
259
+ ...(statement.fields ? { fields: values(statement.fields) } : {}),
260
+ ...expected,
261
+ });
262
+ break;
263
+ }
264
+ case "action": {
265
+ const object = known(
266
+ statement.object,
267
+ objects.keys(),
268
+ "object",
269
+ "create it on an earlier step",
270
+ );
271
+ const kind = objects.get(object)!;
272
+ const exposing = instruments(kind).filter((i) =>
273
+ Object.values(i.actions).some(
274
+ (a) => a.publicAction === statement.action.name,
275
+ ),
276
+ );
277
+ known(
278
+ statement.action,
279
+ exposing.length
280
+ ? [statement.action.name]
281
+ : instruments(kind).flatMap((i) =>
282
+ Object.values(i.actions).flatMap((a) =>
283
+ a.publicAction ? [a.publicAction] : [],
284
+ ),
285
+ ),
286
+ `${kind} action`,
287
+ `name an action object ${kind} exposes`,
288
+ );
289
+ const actor = statement.actor && customer(statement.actor);
290
+ if (statement.state)
291
+ known(
292
+ statement.state,
293
+ exposing.flatMap((i) => i.lifecycle.states),
294
+ "state",
295
+ `name a state ${statement.action.name} can reach`,
296
+ );
297
+ let status: number | "absent" | undefined;
298
+ if (statement.status) {
299
+ const raw = statement.status;
300
+ status =
301
+ raw.kind === "name" && raw.value === "absent"
302
+ ? "absent"
303
+ : raw.kind === "number" && /^[45]\d\d$/.test(raw.value)
304
+ ? Number(raw.value)
305
+ : fail(raw, "expected a refusal", "write status 409 or absent");
306
+ }
307
+ const instance =
308
+ statement.instance &&
309
+ known(
310
+ statement.instance,
311
+ remembered,
312
+ "agreement",
313
+ "remember it on an earlier step",
314
+ );
315
+ const input = statement.input && values(statement.input);
316
+ const fields = statement.fields && values(statement.fields);
317
+ if (statement.remember) {
318
+ if (remembered.has(statement.remember.name))
319
+ fail(statement.remember, "remembered twice", "rename one");
320
+ remembered.add(statement.remember.name);
321
+ }
322
+ steps.push({
323
+ kind: "action",
324
+ object,
325
+ action: statement.action.name,
326
+ ...(actor ? { actor } : {}),
327
+ ...(input ? { input } : {}),
328
+ ...(fields ? { fields } : {}),
329
+ ...(statement.state ? { state: statement.state.name } : {}),
330
+ ...(status !== undefined ? { status } : {}),
331
+ ...(statement.code ? { code: statement.code } : {}),
332
+ ...(statement.remember ? { remember: statement.remember.name } : {}),
333
+ ...(instance ? { instance } : {}),
334
+ ...expected,
335
+ });
336
+ break;
337
+ }
338
+ case "close":
339
+ steps.push({
340
+ kind: "close",
341
+ customer: customer(statement.customer),
342
+ ...expected,
343
+ });
344
+ break;
345
+ case "expect": {
346
+ if (!steps.length)
347
+ fail(statement, "a check needs a step", "add a step before expect");
348
+ if (!statement.balances)
349
+ fail(statement, "expect needs balances", "write expect balances { }");
350
+ final = {
351
+ expected: balances(statement.balances),
352
+ ...(statement.held ? { expectedHeld: money(statement.held) } : {}),
353
+ ...(statement.pending
354
+ ? {
355
+ pending: Object.fromEntries(
356
+ statement.pending.entries.map((entry) => {
357
+ const name = { name: entry.key, span: entry.span };
358
+ if (entry.value.kind !== "block")
359
+ fail(entry, "expected balances", "write name { }");
360
+ const fields = entry.value.entries.map((field) => [
361
+ field.key,
362
+ money(field.value),
363
+ ]);
364
+ return [
365
+ known(name, balanceNames(), "balance", balanceFix),
366
+ Object.fromEntries(fields),
367
+ ];
368
+ }),
369
+ ),
370
+ }
371
+ : {}),
372
+ };
373
+ break;
374
+ }
375
+ }
376
+ }
377
+ if (!final)
378
+ fail(check, "a check ends with expect", "write expect balances { }");
379
+ return {
380
+ name: check.name,
381
+ customers,
382
+ ...(Object.keys(bindings).length ? { bindings } : {}),
383
+ steps,
384
+ ...final,
385
+ };
386
+ }