@hyperscale0/hsx 1.0.130 → 1.0.134

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,aAAa,EAAE,soLAAsoL,CAAC;IACvpL,CAAC,WAAW,EAAE,63KAA63K,CAAC;IAC54K,CAAC,iBAAiB,EAAE,2sNAA2sN,CAAC;IAChuN,CAAC,YAAY,EAAE,60LAA60L,CAAC;IAC71L,CAAC,eAAe,EAAE,8v2EAA8v2E,CAAC;IACjx2E,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,s2HAAs2H,CAAC;CACv3H,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,6wMAA6wM,CAAC;IAC5xM,CAAC,iBAAiB,EAAE,2sNAA2sN,CAAC;IAChuN,CAAC,YAAY,EAAE,yjQAAyjQ,CAAC;IACzkQ,CAAC,eAAe,EAAE,6nrFAA6nrF,CAAC;IAChprF,CAAC,eAAe,EAAE,41LAA41L,CAAC;IAC/2L,CAAC,aAAa,EAAE,03UAA03U,CAAC;IAC34U,CAAC,iBAAiB,EAAE,syTAAsyT,CAAC;IAC3zT,CAAC,WAAW,EAAE,4zYAA4zY,CAAC;IAC30Y,CAAC,eAAe,EAAE,+q2BAA+q2B,CAAC;IACls2B,CAAC,aAAa,EAAE,ohKAAohK,CAAC;IACriK,CAAC,YAAY,EAAE,2zIAA2zI,CAAC;CAC50I,CAAC,CAAC"}
package/docs/README.md CHANGED
@@ -147,6 +147,19 @@ Creation accepts `{}`. Every normalized field, including adapter requirements,
147
147
  is optional at creation. Only a named action requires its subject metadata.
148
148
  Use `attach` inside the object block to configure its financial instruments.
149
149
 
150
+ A required instrument field with no fixed value is a local creation value by
151
+ default. It becomes create `input` on that agreement and never an object field,
152
+ so two agreements on one object keep separate amounts and a wallet spend never
153
+ prefills a later fee. Only a rename such as `rename { dueAt: dueAt }` shares
154
+ it as record metadata: the value then comes from the object field as a create
155
+ subject requirement. An authored object field of the same name does not share
156
+ it; write `rename { price: price }` when the agreement should read that field.
157
+ Later actions still read `self.amount` from the agreement either way. An
158
+ attachment whose create needs input must expose `create`, or compilation fails,
159
+ because a public action on a fresh attachment creates it first with no input.
160
+ A rename binds only fields of the attached instrument itself; renaming a record
161
+ field such as `insurance.cover`'s slice `premium` reports `subject_field_unknown`.
162
+
150
163
  ## Headers and tunables
151
164
 
152
165
  The header defines the available policies. The company chooses percentages, amounts, durations,
@@ -266,7 +279,9 @@ maxAge 1d` requires a recent completed provider check.
266
279
  `moves post self.receipt` posts it and `moves void self.receipt` releases it.
267
280
  `moves amount from payer shares shares` expands a declared split. Optional `fee`,
268
281
  `capture`, `key` and `boundary` modifiers describe a move. `boundary` applies
269
- only to reservations. Fees settle with create moves, never reservations. Repeated clauses keep
282
+ only to reservations. Fees settle with create moves, never reservations. A fee move
283
+ also derives `<action>_<move>_debit`, the payer's whole debit: amount plus fee and
284
+ tax when the buyer pays, the amount alone when the seller pays. Repeated clauses keep
270
285
  their declaration order. The JSON-like clause form remains accepted and lowers
271
286
  to the same [UDL clauses](https://github.com/hyperscale0/hyperscale-udl/blob/main/spec/README.md).
272
287
  Use `economics { purpose: earning, sourceParty: merchant }` on a move to
@@ -407,21 +422,21 @@ Read the instrument's states, time gates and moves before promising a money
407
422
  outcome. A compiler pass does not prove that required actions are exposed,
408
423
  adapters are bound, participants have funds, or the flow can finish.
409
424
 
410
- | Instrument | Behavior |
411
- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
412
- | `financing.installments` | `share` allocates collected profit to a cash payable owned by `capital`. It does not itself pay operator income. |
413
- | `financing.limits` | `per_borrower` caps outstanding principal. Both borrower and portfolio limits must be approved before disbursement; attaching them does not approve them. |
414
- | `lending.round` | The target is the linked plan's principal. Closing requires commitments and held funds to equal that amount. |
415
- | `lending.distribution` | Cash distribution needs an eligible recorded settlement, `prepare_cash`, one share record per funded commitment, then `distribute_cash`. Attaching it moves nothing. |
416
- | `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. |
417
- | `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. |
418
- | `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. |
419
- | `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. |
420
- | `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. |
421
- | `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. |
422
- | `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. At departure the clock completes a paid booking and lapses an unpaid one on `late_forfeit`. |
423
- | `financing.late_charge` | `fine` is a fixed money amount, not a percentage of overdue principal. |
424
- | `cards.card` | `spend_limit` is a per-authorization ceiling, not a monthly aggregate. |
425
+ | Instrument | Behavior |
426
+ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
427
+ | `financing.installments` | `share` allocates collected profit to a cash payable owned by `capital`. It does not itself pay operator income. |
428
+ | `financing.limits` | `per_borrower` caps outstanding principal. Both borrower and portfolio limits must be approved before disbursement; attaching them does not approve them. |
429
+ | `lending.round` | The target is the linked plan's principal. Closing requires commitments and held funds to equal that amount. |
430
+ | `lending.distribution` | Cash distribution needs an eligible recorded settlement, `prepare_cash`, one share record per funded commitment, then `distribute_cash`. Attaching it moves nothing. |
431
+ | `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. |
432
+ | `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. |
433
+ | `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. |
434
+ | `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. |
435
+ | `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. |
436
+ | `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. |
437
+ | `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. At departure the clock completes a paid booking and lapses an unpaid one on `late_forfeit`. |
438
+ | `financing.late_charge` | `fine` is a fixed money amount, not a percentage of overdue principal. |
439
+ | `cards.card` | `spend_limit` is a per-authorization ceiling, not a monthly aggregate. Bind `person` to the cardholder's `holder`, such as `person: actor`; create refuses any other party. The person freezes, unfreezes and cancels; the issuer uses `issuer_freeze`, `issuer_unfreeze` and `issuer_cancel`, and only `issuer_unfreeze` lifts a block. |
425
440
 
426
441
  A seller-owned marketplace object can declare `entryActions: [create_order]`
427
442
  when `create_order` exposes an attachment's create action with its buyer bound
@@ -432,6 +447,14 @@ Without entry actions, the company runs a two-customer sale from its Product
432
447
  key: create the record with `onBehalfOf` naming the seller, then run the buyer's
433
448
  steps with `onBehalfOf` naming the buyer.
434
449
 
450
+ The same rule admits the other std creates whose starting party can be a
451
+ customer: `financing.limits` and `financing.installments` (borrower),
452
+ `insurance.cover` (holder), `wallet.balance` (holder) and `savings.membership`
453
+ (member). Several customers can each start one on the same record. A plan,
454
+ commitment or other reference resolves the sibling that binds the same
455
+ accounts to the parties both share, so each customer's plan finds its own
456
+ escrow and each investor's commitment finds their own wallet.
457
+
435
458
  `financing.limits` and `financing.portfolio_limit` declare `scope: product`.
436
459
  Create and approve the portfolio limit once per Product and the borrower limit
437
460
  once per borrower. Plans on later records resolve those shared references.
@@ -443,8 +466,10 @@ The account primitive creates it automatically and shares it across the Product.
443
466
  Zero-tax escrow releases need no additional party setup.
444
467
 
445
468
  The financing, collections and lending samples expose payment creation, payment,
446
- refund and payoff quotes through the object's existing action routes. A child
447
- alias names the full attachment path, for example:
469
+ refund and payoff quotes through the object's existing action routes. Record
470
+ actions are private: the parent still runs them, but a caller sees one only
471
+ after the program exposes it. A child alias names the full attachment path, for
472
+ example:
448
473
 
449
474
  ```hsx
450
475
  expose enrolment.tuition.payment.create as create_payment
@@ -466,9 +491,10 @@ allocation order and payoff rebate; aliases do not change those terms.
466
491
 
467
492
  The [lending sample](../examples/lending.hsx) also exposes share creation. Callers
468
493
  still need dates, agreement inputs, funded wallets and eligible settlements.
469
- References without an explicit creation input require one matching agreement on
470
- the object. In particular, lending distributions with multiple settlements or
471
- commitments need authored selectors before callers can choose among them.
494
+ Every agreement reference is an optional creation input on an exposed create.
495
+ With one matching agreement on the object the engine picks it; with several,
496
+ such as a plan's slices or a lending round's settlements, the caller passes the
497
+ chosen instance ID under the reference's name.
472
498
  Insurance needs premium-slice records and actions. Savings needs dated
473
499
  contribution records. Read each header's creation and lifecycle requirements.
474
500
 
@@ -531,7 +557,7 @@ An attachment can declare the purpose of an imported action's money:
531
557
 
532
558
  ```hsx
533
559
  attach visit = wallet.spend {
534
- wallet: balance, payee: operator
560
+ wallet: balance, payee: operator, holder: owner
535
561
  economics pay { purpose: earning, sourceParty: owner }
536
562
  expose pay as charge_visit
537
563
  }
@@ -18,10 +18,12 @@ object employee "Employee card" {
18
18
  }
19
19
  attach card = cards.card {
20
20
  expose create as create_card
21
- holder: holder, spend_limit: 5000 SAR
21
+ holder: holder, person: owner, spend_limit: 5000 SAR
22
22
  expose activate as activate_card
23
23
  expose freeze as freeze_card
24
24
  expose unfreeze as unfreeze_card
25
+ expose issuer_freeze as block_card
26
+ expose issuer_unfreeze as unblock_card
25
27
  }
26
28
  attach payment = cards.authorization {
27
29
  expose create as create_payment
@@ -7,14 +7,16 @@ object project "Project" {
7
7
  fields { brief: text, deliverable: text, price: money }
8
8
  columns: [brief, deliverable, price]
9
9
  attach offer = marketplace.listing {
10
+ rename { price: price }
10
11
  expose create as create_offer
11
12
  seller: owner
12
13
  expose publish as publish_offer
13
14
  expose withdraw as withdraw_offer
14
15
  }
15
16
  attach order = marketplace.order {
17
+ rename { price: price }
16
18
  expose create as create_order
17
- listing: offer, buyer: actor
19
+ listing: offer, buyer: actor, fulfiller: owner
18
20
  expose commit as place_order
19
21
  expose fulfill as finish_order
20
22
  }
@@ -45,6 +45,8 @@ object rental "Equipment rental" {
45
45
  fields { equipment: text }
46
46
  columns: [equipment, dueAt]
47
47
  attach security = deposit {
48
+ // dueAt is a column, so it stays shared object metadata.
49
+ rename { dueAt: dueAt }
48
50
  expose create as agree_rental
49
51
  expose fund as pay_deposit
50
52
  expose return_on_time as return_equipment
@@ -15,7 +15,7 @@ object membership "Membership" {
15
15
  }
16
16
  attach visit = wallet.spend {
17
17
  expose create as create_visit
18
- wallet: balance, payee: operator
18
+ wallet: balance, payee: operator, holder: owner
19
19
  economics pay { purpose: earning, sourceParty: owner }
20
20
  expose reserve as reserve_visit
21
21
  expose pay as charge_visit
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperscale0/hsx",
3
- "version": "1.0.130",
3
+ "version": "1.0.134",
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.130",
88
- "@hyperscale0/udl": "1.0.130",
87
+ "@hyperscale0/adl": "1.0.134",
88
+ "@hyperscale0/udl": "1.0.134",
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": "3d768e3b8dd85648cb01257a02fef3b1e0b1ff57"
100
+ "gitHead": "3449c1fe1ea4ee1ae40edc3775ab7bebf890e91b"
101
101
  }
package/src/compile.ts CHANGED
@@ -457,6 +457,9 @@ export function compile(
457
457
  UdlSubjectRequirement,
458
458
  { source: string; span: Span; message: string }
459
459
  >();
460
+ // Instruments that lost a std action to an empty all() parent. Their
461
+ // lifecycles drop the states only that action reached.
462
+ const trimmed = new Set<string>();
460
463
  const used = new Set<string>();
461
464
  for (const use of program.decls.filter((d) => d.kind === "use")) {
462
465
  if (used.has(use.name))
@@ -551,6 +554,15 @@ export function compile(
551
554
  const assignments = new Map<string, AssignmentDecl>();
552
555
  const attachmentSubjects = new Map<string, string>();
553
556
  const names = new Set<string>();
557
+ // A record's create turns public only through a program-level expose,
558
+ // which applies after lowering, so creation input needs the list first.
559
+ const exposedCreates = new Set(
560
+ program.decls.flatMap((decl) =>
561
+ decl.kind === "expose" && decl.target.endsWith(".create")
562
+ ? [decl.target.split(".").slice(0, -1).join("_")]
563
+ : [],
564
+ ),
565
+ );
554
566
  for (const decl of program.decls) {
555
567
  if (decl.kind === "expose" || decl.kind === "hide" || decl.kind === "use")
556
568
  continue;
@@ -2832,6 +2844,15 @@ export function compile(
2832
2844
  } as Omit<UdlCalculation, "target">)
2833
2845
  : undefined;
2834
2846
  const charges = vat ? [charge, vat] : [charge];
2847
+ // A payer's limit reads this: a buyer pays the charges on
2848
+ // top, a seller pays them out of the amount.
2849
+ derived("debit", "Payer debit", {
2850
+ op: "sum",
2851
+ values:
2852
+ paidBy === "buyer"
2853
+ ? [transfer.amount, ...charges]
2854
+ : [transfer.amount],
2855
+ } as Omit<UdlCalculation, "target">);
2835
2856
  const net =
2836
2857
  paidBy === "seller"
2837
2858
  ? derived("net", "Amount after fee", {
@@ -2908,7 +2929,7 @@ export function compile(
2908
2929
  }
2909
2930
  if (actionFeeCalculations.length)
2910
2931
  a.calculate = [...(a.calculate ?? []), ...actionFeeCalculations];
2911
- if (attachmentInfo && !attachmentInfo.child) {
2932
+ if (attachmentInfo) {
2912
2933
  if (attachmentInfo.exposed.has(name)) {
2913
2934
  if (automatic(a.actor))
2914
2935
  fail(
@@ -2975,6 +2996,13 @@ export function compile(
2975
2996
  message: `${id}.${name}.subject.${requirement.field.name}`,
2976
2997
  });
2977
2998
  }
2999
+ // A parent list may name a list parameter such as `[parent, on]`.
3000
+ if (
3001
+ typeof a.actor === "object" &&
3002
+ "parent" in a.actor &&
3003
+ Array.isArray(a.actor.parent)
3004
+ )
3005
+ a.actor.parent = a.actor.parent.flat();
2978
3006
  // A parent actor over an empty all() names no instrument that could
2979
3007
  // invoke the action. A std action then does not exist in this program;
2980
3008
  // in user code the all() is a mistake.
@@ -2993,6 +3021,7 @@ export function compile(
2993
3021
  );
2994
3022
  }
2995
3023
  delete lifecycle.transitions[name];
3024
+ trimmed.add(id);
2996
3025
  continue;
2997
3026
  }
2998
3027
  inst.actions[name] = a;
@@ -3282,8 +3311,7 @@ export function compile(
3282
3311
  .slice(firstAttachedInstrument)
3283
3312
  .filter(
3284
3313
  (instrument) =>
3285
- instrument.id === instId ||
3286
- instrument.actions.create?.publicAction,
3314
+ instrument.id === instId || exposedCreates.has(instrument.id),
3287
3315
  )) {
3288
3316
  if (!createdInst.actions.create) continue;
3289
3317
  const owned = new Set(
@@ -3296,9 +3324,15 @@ export function compile(
3296
3324
  }
3297
3325
  const create = createdInst.actions.create;
3298
3326
  for (const field of createdInst.fields) {
3327
+ // The engine resolves a lone candidate itself; with several (a
3328
+ // plan's slices) the caller names one, as late_charge does.
3329
+ if (field.type === "ref" && field.targetKind === "instrument") {
3330
+ if (!create.input.some((input) => input.name === field.name))
3331
+ create.input.push({ ...field, optional: true });
3332
+ continue;
3333
+ }
3299
3334
  if (
3300
3335
  field.type === "account" ||
3301
- (field.type === "ref" && field.targetKind === "instrument") ||
3302
3336
  (field.type === "list" &&
3303
3337
  field.item === "ref" &&
3304
3338
  field.targetKind === "instrument") ||
@@ -3308,8 +3342,7 @@ export function compile(
3308
3342
  create.input.some((input) => input.name === field.name)
3309
3343
  )
3310
3344
  continue;
3311
- create.subject ??= { requirements: [], adapters: [] };
3312
- const existing = create.subject.requirements.find(
3345
+ const existing = create.subject?.requirements.find(
3313
3346
  (item) => item.field.name === field.name,
3314
3347
  );
3315
3348
  if (existing) {
@@ -3322,11 +3355,16 @@ export function compile(
3322
3355
  );
3323
3356
  continue;
3324
3357
  }
3358
+ // A creation value belongs to this agreement. Only a rename shares
3359
+ // it as object metadata; a matching authored field name does not,
3360
+ // so two agreements never read each other's amount.
3325
3361
  const objectField = renames.get(field.name);
3326
- const requirement = {
3327
- field,
3328
- ...(objectField ? { objectField } : {}),
3329
- };
3362
+ if (!objectField) {
3363
+ create.input.push({ ...field });
3364
+ continue;
3365
+ }
3366
+ const requirement = { field, objectField };
3367
+ create.subject ??= { requirements: [], adapters: [] };
3330
3368
  create.subject.requirements.push(requirement);
3331
3369
  requirementOrigins.set(requirement, {
3332
3370
  source: declarationSources.get(template) ?? "program",
@@ -3345,6 +3383,20 @@ export function compile(
3345
3383
  );
3346
3384
  if (!found) {
3347
3385
  const renameEntry = renameEntries.get(oldName) ?? entry;
3386
+ const record = document.instruments
3387
+ .slice(firstAttachedInstrument)
3388
+ .find(
3389
+ (instrument) =>
3390
+ instrument.id.startsWith(`${instId}_`) &&
3391
+ instrument.fields.some((field) => field.name === oldName),
3392
+ );
3393
+ if (record)
3394
+ failWithCode(
3395
+ renameEntry,
3396
+ "subject_field_unknown",
3397
+ `rename source '${oldName}' belongs to the ${record.id.slice(instId.length + 1)} record of ${targetTemplate}; rename binds only fields of ${targetTemplate} itself`,
3398
+ `remove this rename; supply ${oldName} as input when the record is created, or set it as a tunable where the header offers one`,
3399
+ );
3348
3400
  const declared = attachedInst
3349
3401
  ? Object.values(attachedInst.actions).flatMap(
3350
3402
  (action) =>
@@ -3889,9 +3941,21 @@ export function compile(
3889
3941
  targets.some((target) => kin(target, candidate)),
3890
3942
  );
3891
3943
  });
3892
- // A count over no matchable member is zero.
3944
+ // A count over no matchable member is zero, in a requirement or a calculation.
3893
3945
  for (const inst of document.instruments)
3894
- for (const action of Object.values(inst.actions))
3946
+ for (const action of Object.values(inst.actions)) {
3947
+ if (action.calculate)
3948
+ action.calculate = action.calculate.map((calculation) =>
3949
+ calculation.op === "aggregate" &&
3950
+ calculation.measure === "count" &&
3951
+ !matchable(calculation.selection, inst, action).length
3952
+ ? {
3953
+ target: calculation.target,
3954
+ op: "sum",
3955
+ values: [{ literal: 0 }],
3956
+ }
3957
+ : calculation,
3958
+ );
3895
3959
  action.requires = action.requires.map((requirement) =>
3896
3960
  requirement.kind === "aggregate" &&
3897
3961
  requirement.measure === "count" &&
@@ -3904,6 +3968,7 @@ export function compile(
3904
3968
  }
3905
3969
  : requirement,
3906
3970
  );
3971
+ }
3907
3972
  const checkSelection = (
3908
3973
  selection: UdlSelection,
3909
3974
  at: Located,
@@ -4076,7 +4141,7 @@ export function compile(
4076
4141
  )
4077
4142
  )
4078
4143
  continue;
4079
- let specialized = false;
4144
+ let specialized = trimmed.has(inst.id);
4080
4145
  for (const [key, action] of Object.entries(inst.actions)) {
4081
4146
  const constant = (value: import("@hyperscale0/udl").UdlValue) => {
4082
4147
  if ("literal" in value) return value.literal;
@@ -4194,6 +4259,28 @@ export function compile(
4194
4259
  "give each exposed action a distinct public name",
4195
4260
  );
4196
4261
  }
4262
+ // A public action on a fresh attachment creates it first with no input
4263
+ // (in the engine executor), so creation that needs input must be public.
4264
+ for (const kind of document.objects)
4265
+ for (const attachment of kind.attachments) {
4266
+ if (attachment.parent) continue;
4267
+ const instrument = byId.get(attachment.instrument)!;
4268
+ const create = instrument.actions.create;
4269
+ const needed = create?.input.filter((field) => !field.optional) ?? [];
4270
+ const entry = Object.values(instrument.actions).find(
4271
+ (action) => action.publicAction,
4272
+ );
4273
+ if (!needed.length || create!.publicAction || !entry) continue;
4274
+ fail(
4275
+ objects
4276
+ .get(kind.id)!
4277
+ .body.entries.find((row) =>
4278
+ new RegExp(`^attach\\s+${attachment.name}\\s*=`).test(row.key),
4279
+ ) ?? objects.get(kind.id)!,
4280
+ `${attachment.name} starts from ${entry.publicAction}, but its create needs ${needed.map((field) => field.name).join(", ")} and is not exposed`,
4281
+ `expose create on ${attachment.name} so the caller supplies ${needed.length === 1 ? "it" : "them"}, or rename { ${needed[0]!.name}: ${needed[0]!.name} } to read object metadata`,
4282
+ );
4283
+ }
4197
4284
  for (const kind of document.objects) {
4198
4285
  for (const name of kind.entryActions ?? []) {
4199
4286
  const eligible = kind.attachments.some((attachment) => {
@@ -1,19 +1,19 @@
1
1
  // Generated by open/hsx/scripts/bundle-examples.ts. Do not edit by hand.
2
2
  import type { HsxExample } from "./examples.ts";
3
3
  export const examples: HsxExample[] = [
4
- {"id":"cards","title":"Team expense cards","summary":"A company issues expense cards to employees and reserves each supplier payment before capture.","headers":["cards"],"source":"// A company issues expense cards to employees and reserves each supplier payment before capture.\nprogram team_expense_cards \"Team expense cards\"\nuse cards\n// supplier is a business this program declares, so plan needs {\"partyBindings\":{\"supplier\":\"<partyId>\"}}.\n// That partyId is an organization customer's: create the Product from this source without the payment\n// and receipt attachments, add the customer with customer.create, activate its access, open its\n// customer_balance account, then compose this source with the binding.\nparty supplier: business\n\nobject employee \"Employee card\" {\n fields { employeeName: text, department: text }\n columns: [employeeName, department]\n attach holder = cards.cardholder {\n expose create as create_holder\n holder: owner\n expose activate as activate_employee\n expose suspend as suspend_employee\n }\n attach card = cards.card {\n expose create as create_card\n holder: holder, spend_limit: 5000 SAR\n expose activate as activate_card\n expose freeze as freeze_card\n expose unfreeze as unfreeze_card\n }\n attach payment = cards.authorization {\n expose create as create_payment\n card: card, merchant: supplier\n expose approve as approve_payment\n expose capture as capture_payment\n expose reverse as reverse_payment\n }\n attach receipt = cards.transaction {\n expose create as create_receipt\n authorization: payment\n expose refund as refund_payment\n }\n}\n"},
4
+ {"id":"cards","title":"Team expense cards","summary":"A company issues expense cards to employees and reserves each supplier payment before capture.","headers":["cards"],"source":"// A company issues expense cards to employees and reserves each supplier payment before capture.\nprogram team_expense_cards \"Team expense cards\"\nuse cards\n// supplier is a business this program declares, so plan needs {\"partyBindings\":{\"supplier\":\"<partyId>\"}}.\n// That partyId is an organization customer's: create the Product from this source without the payment\n// and receipt attachments, add the customer with customer.create, activate its access, open its\n// customer_balance account, then compose this source with the binding.\nparty supplier: business\n\nobject employee \"Employee card\" {\n fields { employeeName: text, department: text }\n columns: [employeeName, department]\n attach holder = cards.cardholder {\n expose create as create_holder\n holder: owner\n expose activate as activate_employee\n expose suspend as suspend_employee\n }\n attach card = cards.card {\n expose create as create_card\n holder: holder, person: owner, spend_limit: 5000 SAR\n expose activate as activate_card\n expose freeze as freeze_card\n expose unfreeze as unfreeze_card\n expose issuer_freeze as block_card\n expose issuer_unfreeze as unblock_card\n }\n attach payment = cards.authorization {\n expose create as create_payment\n card: card, merchant: supplier\n expose approve as approve_payment\n expose capture as capture_payment\n expose reverse as reverse_payment\n }\n attach receipt = cards.transaction {\n expose create as create_receipt\n authorization: payment\n expose refund as refund_payment\n }\n}\n"},
5
5
  {"id":"collections","title":"Small business collections","summary":"A business lender refers overdue instalments to an agency with limits on collection contacts.","headers":["collections","financing"],"source":"// A business lender refers overdue instalments to an agency with limits on collection contacts.\nprogram business_collections \"Small business collections\"\nuse collections\nuse financing\n// agency is a business this program declares, so plan needs {\"partyBindings\":{\"agency\":\"<partyId>\"}}.\n// That partyId is an organization customer's: create the Product from this source without the recovery\n// and contact attachments, add the customer with customer.create, activate its access, open its\n// customer_balance account, then compose this source with the binding.\nparty agency: business\n\nobject facility \"Business loan\" {\n fields { businessName: text, purpose: text, price: money }\n columns: [businessName, purpose, price]\n attach allowance = financing.limits { expose create as create_allowance, expose approve as approve_borrower, borrower: owner, per_borrower: 100000 SAR }\n attach budget = financing.portfolio_limit { expose create as create_budget, expose approve as approve_budget, limit: 2000000 SAR }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_loan\n expose disburse as release_loan\n expose payoff as repay_early\n }\n attach recovery = collections.case {\n expose create as create_recovery\n on: loan, agency: agency\n expose assign as assign_case\n expose recover as recover_payment\n expose recall as recall_case\n }\n attach contact = collections.contact {\n expose create as create_contact\n case: recovery, agency: agency\n }\n}\n\nexpose facility.loan.payment.create as create_payment\nexpose facility.loan.payment.pay as pay_instalment\nexpose facility.loan.payment.refund as refund_payment\nexpose facility.loan.payoff_quote.create as request_payoff_quote\n"},
6
6
  {"id":"escrow","title":"Used device sales","summary":"Buyers fund a used device purchase, then accept delivery or raise a dispute before release.","headers":["escrow"],"source":"// Buyers fund a used device purchase, then accept delivery or raise a dispute before release.\nprogram used_device_sales \"Used device sales\"\nuse escrow\n\nobject device \"Device\" {\n fields {\n model: text\n condition: text\n serialNumber: text\n price: money\n }\n columns: [model, condition, price]\n attach sale = escrow.hold {\n expose create as create_sale\n payer: actor, payee: owner\n expose fund as pay_for_device\n expose deliver as confirm_delivery\n expose accept as accept_device\n expose dispute as report_problem\n expose verify_return as verify_return\n expose refund as refund_buyer\n expose cancel as cancel_sale\n }\n}\n"},
7
7
  {"id":"financing","title":"Tuition instalments","summary":"A tuition lender funds a student's fees over three interest-free instalments with borrowing limits.","headers":["financing"],"source":"// A tuition lender funds a student's fees over three interest-free instalments with borrowing limits.\nprogram tuition_instalments \"Tuition instalments\"\nuse financing\n\nobject enrolment \"Enrolment\" {\n fields { student: text, course: text, price: money }\n columns: [student, course, price]\n attach allowance = financing.limits {\n expose create as create_allowance\n borrower: owner, per_borrower: 12000 SAR\n expose approve as approve_student\n }\n attach budget = financing.portfolio_limit {\n expose create as create_budget\n limit: 500000 SAR\n expose approve as approve_budget\n }\n attach tuition = financing.installments {\n expose create as create_tuition\n borrower: owner, capital: operator\n months: 3, pricing: flat_total, profit_rate: 0%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_tuition_plan\n expose disburse as fund_tuition\n expose payoff as repay_early\n }\n}\n\nexpose enrolment.tuition.payment.create as create_payment\nexpose enrolment.tuition.payment.pay as pay_instalment\nexpose enrolment.tuition.payment.refund as refund_payment\nexpose enrolment.tuition.payoff_quote.create as request_payoff_quote\n"},
8
8
  {"id":"insurance","title":"Device insurance","summary":"A device retailer collects the purchase price and attaches cover with a claim review process.","headers":["insurance","money"],"source":"// A device retailer collects the purchase price and attaches cover with a claim review process.\nprogram device_insurance \"Device insurance\"\nuse insurance\nuse money\n\nobject device \"Insured device\" {\n fields { model: text, serialNumber: text, purchaseDate: date }\n columns: [model, serialNumber, purchaseDate]\n attach purchase = money.transfer {\n expose create as create_purchase\n payer: owner, payee: operator\n economics pay { purpose: earning, sourceParty: owner }\n expose pay as pay_for_device\n }\n // conformance_boundary is the sandbox insurer adapter; swap in the insurer's registered adapter to go live.\n // Premiums net of commission go to its account. The default broker is the retailer, which keeps the commission.\n // claim_limit is the sum insured, set per cover at creation.\n attach protection = insurance.cover {\n expose create as create_protection\n holder: owner, adapter: \"conformance_boundary\", commission: 10%, covers: purchase\n expose activate as activate_cover\n expose cancel as cancel_cover\n }\n attach claim = insurance.claim {\n expose create as create_claim\n cover: protection, inspector: operator\n expose approve as approve_claim\n expose deny as deny_claim\n expose pay as pay_claim\n }\n}\n"},
9
9
  {"id":"lending","title":"Community business lending","summary":"A lending composition with limit approvals, wallet commitments and cash distribution transitions.","headers":["lending","financing","wallet"],"source":"// A lending composition with limit approvals, wallet commitments and cash distribution transitions.\nprogram community_business_lending \"Community business lending\"\nuse lending\nuse financing\nuse wallet\n\nobject business \"Business funding\" {\n fields { businessName: text, purpose: text, price: money }\n columns: [businessName, purpose, price]\n attach allowance = financing.limits {\n expose create as create_allowance\n borrower: owner, per_borrower: 100000 SAR\n expose approve as approve_borrower\n }\n attach budget = financing.portfolio_limit {\n expose create as create_budget\n limit: 2000000 SAR\n expose approve as approve_budget\n }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_loan\n expose disburse as release_loan\n expose payoff as repay_early\n }\n attach investor_balance = wallet.balance {\n expose create as create_investor_balance\n holder: actor\n expose activate as open_wallet\n expose topup as add_funds\n }\n attach funding = lending.round {\n borrower: owner, plan: loan\n expose create as open_funding\n expose close as close_funding\n }\n attach investment = lending.commitment {\n round: funding, wallet: investor_balance, investor: actor\n expose create as invest\n expose withdraw as withdraw_investment\n }\n attach returns = lending.distribution {\n expose create as create_returns\n round: funding, receipt: loan.settlement\n expose prepare_cash as prepare_returns\n expose distribute_cash as distribute_returns\n }\n}\n\nexpose business.loan.payment.create as create_payment\nexpose business.loan.payment.pay as pay_instalment\nexpose business.loan.payment.refund as refund_payment\nexpose business.loan.payoff_quote.create as request_payoff_quote\nexpose business.returns.share.create as create_return_share\n"},
10
10
  {"id":"library","title":"Financed car sales","summary":"A minimal financed car sale connects escrow to a three-instalment plan and lending limits.","headers":["escrow","financing"],"source":"// A minimal financed car sale connects escrow to a three-instalment plan and lending limits.\nprogram cars \"Financed car sales\"\nuse escrow\nuse financing\nobject car \"Cars\" {\n fields { make: text, model: text, year: integer }\n attach sale = escrow.hold { expose create as create_sale, payer: actor, payee: owner, expose fund as sell }\n attach limits = financing.limits { expose create as create_limits, borrower: actor, per_borrower: 60000 SAR }\n attach ceiling = financing.portfolio_limit { expose create as create_ceiling, limit: 1500000 SAR }\n attach plan = financing.installments {\n borrower: actor, capital: operator, share: 25%\n months: 3, pricing: flat_total, profit_rate: 2.5%, funds: sale, limits: limits, portfolio: ceiling\n expose create as finance\n }\n}\n"},
11
- {"id":"marketplace","title":"Freelance marketplace","summary":"Freelancers publish offers and clients place orders with funds held until work is accepted.","headers":["marketplace","escrow"],"source":"// Freelancers publish offers and clients place orders with funds held until work is accepted.\nprogram freelance_marketplace \"Freelance marketplace\"\nuse marketplace\nuse escrow\n\nobject project \"Project\" {\n fields { brief: text, deliverable: text, price: money }\n columns: [brief, deliverable, price]\n attach offer = marketplace.listing {\n expose create as create_offer\n seller: owner\n expose publish as publish_offer\n expose withdraw as withdraw_offer\n }\n attach order = marketplace.order {\n expose create as create_order\n listing: offer, buyer: actor\n expose commit as place_order\n expose fulfill as finish_order\n }\n attach payment = escrow.hold {\n expose create as create_payment\n payer: actor, payee: owner\n expose fund as fund_project\n expose deliver as submit_work\n expose accept as accept_work\n expose dispute as dispute_work\n expose verify_return as verify_return\n expose refund as refund_client\n expose cancel as cancel_payment\n }\n}\n"},
11
+ {"id":"marketplace","title":"Freelance marketplace","summary":"Freelancers publish offers and clients place orders with funds held until work is accepted.","headers":["marketplace","escrow"],"source":"// Freelancers publish offers and clients place orders with funds held until work is accepted.\nprogram freelance_marketplace \"Freelance marketplace\"\nuse marketplace\nuse escrow\n\nobject project \"Project\" {\n fields { brief: text, deliverable: text, price: money }\n columns: [brief, deliverable, price]\n attach offer = marketplace.listing {\n rename { price: price }\n expose create as create_offer\n seller: owner\n expose publish as publish_offer\n expose withdraw as withdraw_offer\n }\n attach order = marketplace.order {\n rename { price: price }\n expose create as create_order\n listing: offer, buyer: actor, fulfiller: owner\n expose commit as place_order\n expose fulfill as finish_order\n }\n attach payment = escrow.hold {\n expose create as create_payment\n payer: actor, payee: owner\n expose fund as fund_project\n expose deliver as submit_work\n expose accept as accept_work\n expose dispute as dispute_work\n expose verify_return as verify_return\n expose refund as refund_client\n expose cancel as cancel_payment\n }\n}\n"},
12
12
  {"id":"money","title":"Tutoring studio","summary":"A tutoring studio collects a fixed fee for each lesson and can cancel unpaid bookings.","headers":["money"],"source":"// A tutoring studio collects a fixed fee for each lesson and can cancel unpaid bookings.\nprogram tutoring_studio \"Tutoring studio\"\nuse money\n\nobject lesson \"Lesson\" {\n fields {\n student: text\n tutor: text\n startsAt: date\n }\n columns: [student, tutor, startsAt]\n attach payment = money.transfer {\n expose create as create_payment\n payer: actor, payee: operator, amount: 150 SAR\n economics pay { purpose: earning, sourceParty: actor }\n expose pay as pay_for_lesson\n expose cancel as cancel_payment\n }\n}\n"},
13
- {"id":"rental-deposit","title":"Equipment rental","summary":"A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee. On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.","headers":[],"source":"// A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee.\n// On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.\nprogram equipment_rental \"Equipment rental\"\nparty renter: business\n\n// The renter is one declared business, so this instrument takes no parameters.\ninstrument deposit {\n summary: \"Hold a rental deposit, then return it less any fixed late fee.\"\n fields {\n renter: account of renter\n held: account of self\n amount: money = 1000 SAR\n lateFee: money = 50 SAR\n dueAt: date\n refund: money?\n }\n lifecycle { states: [pending, funded, returned, cancelled], initial: pending }\n action create {\n actor: { party: renter }\n requires self.dueAt > self.now\n }\n action fund {\n from: pending, to: funded, actor: { party: renter }\n deadline: { at: self.dueAt }\n moves self.amount from self.renter to self.held\n }\n action return_on_time {\n from: funded, to: returned, actor: { party: programOperator }\n deadline: { at: self.dueAt }\n subject { returnReference: text }\n moves self.held.balance from self.held to self.renter\n }\n action return_late {\n from: funded, to: returned, actor: { party: programOperator }\n due: { at: self.dueAt }\n subject { returnReference: text }\n calculate: [{ target: refund, op: subtract, base: { field: self.held.balance }, subtract: [{ field: self.lateFee }] }]\n moves self.lateFee from self.held to programOperator\n moves self.refund from self.held to self.renter\n }\n action cancel { from: pending, to: cancelled, actor: { party: renter } }\n}\n\nobject rental \"Equipment rental\" {\n fields { equipment: text }\n columns: [equipment, dueAt]\n attach security = deposit {\n expose create as agree_rental\n expose fund as pay_deposit\n expose return_on_time as return_equipment\n expose return_late as return_equipment_late\n expose cancel as cancel_rental\n }\n}\n\nhide deposit.create\nhide deposit.fund\nhide deposit.return_on_time\nhide deposit.return_late\nhide deposit.cancel\n"},
13
+ {"id":"rental-deposit","title":"Equipment rental","summary":"A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee. On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.","headers":[],"source":"// A business rents equipment with a 1000 SAR deposit and a single 50 SAR late fee.\n// On-time return refunds 1000 SAR; late return pays the operator 50 SAR and refunds 950 SAR.\nprogram equipment_rental \"Equipment rental\"\nparty renter: business\n\n// The renter is one declared business, so this instrument takes no parameters.\ninstrument deposit {\n summary: \"Hold a rental deposit, then return it less any fixed late fee.\"\n fields {\n renter: account of renter\n held: account of self\n amount: money = 1000 SAR\n lateFee: money = 50 SAR\n dueAt: date\n refund: money?\n }\n lifecycle { states: [pending, funded, returned, cancelled], initial: pending }\n action create {\n actor: { party: renter }\n requires self.dueAt > self.now\n }\n action fund {\n from: pending, to: funded, actor: { party: renter }\n deadline: { at: self.dueAt }\n moves self.amount from self.renter to self.held\n }\n action return_on_time {\n from: funded, to: returned, actor: { party: programOperator }\n deadline: { at: self.dueAt }\n subject { returnReference: text }\n moves self.held.balance from self.held to self.renter\n }\n action return_late {\n from: funded, to: returned, actor: { party: programOperator }\n due: { at: self.dueAt }\n subject { returnReference: text }\n calculate: [{ target: refund, op: subtract, base: { field: self.held.balance }, subtract: [{ field: self.lateFee }] }]\n moves self.lateFee from self.held to programOperator\n moves self.refund from self.held to self.renter\n }\n action cancel { from: pending, to: cancelled, actor: { party: renter } }\n}\n\nobject rental \"Equipment rental\" {\n fields { equipment: text }\n columns: [equipment, dueAt]\n attach security = deposit {\n // dueAt is a column, so it stays shared object metadata.\n rename { dueAt: dueAt }\n expose create as agree_rental\n expose fund as pay_deposit\n expose return_on_time as return_equipment\n expose return_late as return_equipment_late\n expose cancel as cancel_rental\n }\n}\n\nhide deposit.create\nhide deposit.fund\nhide deposit.return_on_time\nhide deposit.return_late\nhide deposit.cancel\n"},
14
14
  {"id":"repair-approval","title":"Repair approvals","summary":"A repair shop records inspection and approval evidence before it can complete a job.","headers":[],"source":"// A repair shop records inspection and approval evidence before it can complete a job.\nprogram repair_shop \"Repair approvals\"\n\ninstrument assessment {\n summary: \"Inspect the repair, collect approval, then record completion.\"\n fields { inspectedAt: date?, approvedAt: date? }\n lifecycle { states: [submitted, inspected, approved, completed, rejected], initial: submitted }\n action create {}\n action inspect {\n from: submitted, to: inspected, actor: { party: programOperator }\n subject { inspectionNotes: text }\n set: { inspectedAt: { field: self.now } }\n }\n action approve {\n from: inspected, to: approved, actor: { party: programOperator }\n subject { approvalReference: text }\n set: { approvedAt: { field: self.now } }\n }\n action reject {\n from: inspected, to: rejected, actor: { party: programOperator }\n }\n action complete {\n from: approved, to: completed, actor: { party: programOperator }\n subject { completionNotes: text }\n }\n}\n\nobject repair \"Repair\" {\n fields { item: text, problem: text }\n columns: [item, problem]\n attach review = assessment {\n expose create as create_review\n expose inspect as inspect_repair\n expose approve as approve_repair\n expose reject as reject_repair\n expose complete as complete_repair\n }\n}\n\nhide assessment.create\nhide assessment.inspect\nhide assessment.approve\nhide assessment.reject\nhide assessment.complete\n"},
15
15
  {"id":"reporting","title":"Equipment loan reports","summary":"An equipment lender keeps an ageing report over its financed purchases and recorded repayments.","headers":["reporting","financing"],"source":"// An equipment lender keeps an ageing report over its financed purchases and recorded repayments.\nprogram equipment_loan_reports \"Equipment loan reports\"\nuse reporting\nuse financing\n\nobject purchase \"Equipment loan\" {\n fields { businessName: text, equipment: text, price: money }\n columns: [businessName, equipment, price]\n attach allowance = financing.limits {\n expose create as create_allowance\n borrower: owner, per_borrower: 200000 SAR\n expose approve as approve_business\n }\n attach budget = financing.portfolio_limit {\n expose create as create_budget\n limit: 5000000 SAR\n expose approve as approve_budget\n }\n attach loan = financing.installments {\n expose create as create_loan\n borrower: owner, capital: operator\n months: 12, pricing: amortizing, profit_rate: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_purchase_loan\n expose disburse as fund_purchase\n }\n attach reports = reporting.portfolio { expose create as create_reports, on: loan }\n}\n"},
16
16
  {"id":"savings","title":"Savings circle","summary":"Six members contribute to a shared pot and receive it in their agreed calendar order.","headers":["savings"],"source":"// Six members contribute to a shared pot and receive it in their agreed calendar order.\nprogram neighbourhood_savings \"Savings circle\"\nuse savings\n\nobject group \"Savings group\" {\n fields { groupName: text, purpose: text }\n columns: [groupName, purpose]\n attach circle = savings.circle {\n expose create as create_circle\n contribution: 500 SAR, members: 6, starts: 2027-01-01\n expose disband as disband_circle\n expose close as close_circle\n }\n attach membership = savings.membership {\n expose create as create_membership\n circle: circle, member: actor\n expose activate as confirm_membership\n expose withdraw as leave_circle\n }\n // Contributions and each member's payout follow the declared calendar.\n}\n"},
17
17
  {"id":"serviced","title":"Car financing","summary":"A car finance company services loans with late charges, payment reminders and early repayment.","headers":["escrow","financing","collections"],"source":"// A car finance company services loans with late charges, payment reminders and early repayment.\nprogram car_financing \"Car financing\"\nuse escrow\nuse financing\nuse collections\nobject car \"Car\" {\n fields { make: text, model: text, year: integer, vin: text, price: money }\n columns: [make, model, year, vin, price]\n attach sale = escrow.hold { expose create as create_sale, payer: actor, payee: operator, expose fund as purchase }\n attach limits = financing.limits { expose create as create_limits, borrower: actor, per_borrower: 250000 SAR }\n attach ceiling = financing.portfolio_limit { expose create as create_ceiling, limit: 10000000 SAR }\n attach plan = financing.installments {\n borrower: actor, capital: operator\n months: 36, pricing: flat_total, profit_rate: 8.5%, down_payment: 10%\n apply: fines_profit_principal, payoff_rebate: 100%, write_off_after: 90d\n funds: sale, limits: limits, portfolio: ceiling\n expose create as finance, expose sign as sign_contract, expose payoff as payoff\n }\n attach late = financing.late_charge {\n expose create as create_late\n on: plan, grace: 3d, fine: 50 SAR, cap: 25 SAR\n fines_to: operator, costs_to: operator, borrower: actor\n }\n attach reminders = collections.reminder { expose create as create_reminders, on: plan, operator: operator, before_days: 3, overdue_days: 1 }\n}\n"},
18
- {"id":"wallet","title":"Workshop credits","summary":"Members prepay for workshop visits and the studio reserves each spend before charging it.","headers":["wallet"],"source":"// Members prepay for workshop visits and the studio reserves each spend before charging it.\nprogram workshop_credits \"Workshop credits\"\nuse wallet\n\nobject membership \"Membership\" {\n fields { memberName: text, membershipNumber: text }\n columns: [memberName, membershipNumber]\n attach balance = wallet.balance {\n expose create as create_balance\n holder: owner\n expose activate as open_balance\n expose topup as add_credit\n expose withdraw as withdraw_credit\n expose close as close_balance\n }\n attach visit = wallet.spend {\n expose create as create_visit\n wallet: balance, payee: operator\n economics pay { purpose: earning, sourceParty: owner }\n expose reserve as reserve_visit\n expose pay as charge_visit\n expose cancel as cancel_visit\n }\n}\n"},
18
+ {"id":"wallet","title":"Workshop credits","summary":"Members prepay for workshop visits and the studio reserves each spend before charging it.","headers":["wallet"],"source":"// Members prepay for workshop visits and the studio reserves each spend before charging it.\nprogram workshop_credits \"Workshop credits\"\nuse wallet\n\nobject membership \"Membership\" {\n fields { memberName: text, membershipNumber: text }\n columns: [memberName, membershipNumber]\n attach balance = wallet.balance {\n expose create as create_balance\n holder: owner\n expose activate as open_balance\n expose topup as add_credit\n expose withdraw as withdraw_credit\n expose close as close_balance\n }\n attach visit = wallet.spend {\n expose create as create_visit\n wallet: balance, payee: operator, holder: owner\n economics pay { purpose: earning, sourceParty: owner }\n expose reserve as reserve_visit\n expose pay as charge_visit\n expose cancel as cancel_visit\n }\n}\n"},
19
19
  ];