@hyperscale0/hsx 5.0.1 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"std-bundle.js","sourceRoot":"","sources":["../../src/std-bundle.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,MAAM,CAAC,MAAM,iBAAiB,GAAgC,IAAI,GAAG,CAAiB;IACpF,CAAC,WAAW,EAAE,i8HAAi8H,CAAC;IACh9H,CAAC,iBAAiB,EAAE,ooKAAooK,CAAC;IACzpK,CAAC,YAAY,EAAE,o8DAAo8D,CAAC;IACp9D,CAAC,eAAe,EAAE,m5vDAAm5vD,CAAC;IACt6vD,CAAC,eAAe,EAAE,g7FAAg7F,CAAC;IACn8F,CAAC,aAAa,EAAE,8pPAA8pP,CAAC;IAC/qP,CAAC,iBAAiB,EAAE,m0PAAm0P,CAAC;IACx1P,CAAC,WAAW,EAAE,ytRAAytR,CAAC;IACxuR,CAAC,eAAe,EAAE,gs5CAAgs5C,CAAC;IACnt5C,CAAC,aAAa,EAAE,wrGAAwrG,CAAC;IACzsG,CAAC,YAAY,EAAE,8kMAA8kM,CAAC;IAC9lM,CAAC,YAAY,EAAE,m6DAAm6D,CAAC;CACp7D,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,WAAW,EAAE,uhIAAuhI,CAAC;IACtiI,CAAC,iBAAiB,EAAE,ooKAAooK,CAAC;IACzpK,CAAC,YAAY,EAAE,o8DAAo8D,CAAC;IACp9D,CAAC,eAAe,EAAE,o6vDAAo6vD,CAAC;IACv7vD,CAAC,eAAe,EAAE,+0GAA+0G,CAAC;IACl2G,CAAC,aAAa,EAAE,mvPAAmvP,CAAC;IACpwP,CAAC,iBAAiB,EAAE,m0PAAm0P,CAAC;IACx1P,CAAC,WAAW,EAAE,y1RAAy1R,CAAC;IACx2R,CAAC,eAAe,EAAE,w77CAAw77C,CAAC;IAC387C,CAAC,aAAa,EAAE,wrGAAwrG,CAAC;IACzsG,CAAC,YAAY,EAAE,8kMAA8kM,CAAC;IAC9lM,CAAC,YAAY,EAAE,m6DAAm6D,CAAC;CACp7D,CAAC,CAAC"}
@@ -1,3 +1,3 @@
1
- export declare const HSX_VERSION = "5.0.1";
1
+ export declare const HSX_VERSION = "5.2.0";
2
2
  export declare const HSX_TARGET_UDL_VERSION = 4;
3
3
  //# sourceMappingURL=version.d.ts.map
@@ -1,3 +1,3 @@
1
- export const HSX_VERSION = "5.0.1";
1
+ export const HSX_VERSION = "5.2.0";
2
2
  export const HSX_TARGET_UDL_VERSION = 4;
3
3
  //# sourceMappingURL=version.js.map
package/docs/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # HSX 4
1
+ # HSX 5.0.1
2
2
 
3
3
  A program declares business objects and financial instruments. Objects contain
4
4
  optional metadata. Attachments expose named actions against an object.
@@ -69,8 +69,22 @@ keeps it internal. Callers create objects and run their attached actions.
69
69
  Accounts use cash or claim books. A move stays in one book. `account of buyer`
70
70
  aliases the default cash account; `account of self` provisions an owned account.
71
71
  `account(lender, cash, "capital")` binds a named account.
72
+ `account(adapter(insurer), cash, "premium")` binds the account of the provider
73
+ selected by `insurer`. Declare that binding with `subject { adapter: insurer }`
74
+ in an action of the same instrument. A text tunable may supply its binding name.
75
+ The agreement retains the provider identity; aliases for that provider share the
76
+ same book and key within the Product. Providers remain outside parties.
77
+ A missing binding refuses before money moves. Reference paths such as
78
+ `self.cover.insurer` use the referenced agreement's account.
79
+
80
+ Insurance collection credits the premium to the operator, then moves the premium
81
+ net of commission to the insurer account. Refunds reverse those two portions.
82
+ Claims reserve from the cover's insurer account. These moves record ledger money;
83
+ external confirmation still needs the boundary protocol.
72
84
  `account(buyer, claim, contra, "debt")` declares the borrower's claim contra account.
73
- `account(seller, cash, external, "bank")` binds the executor-managed bank destination.
85
+ Provider confirmation belongs on a reserved move with `boundary adapter`,
86
+ followed by instruction-bound evidence and a post or void. External account
87
+ mode is not supported.
74
88
  Outstanding debt is an account balance. The library pairs cash repayment with
75
89
  claim reduction and represents receipts as immutable child records. Cash and loss
76
90
  shares round down; the declared residual account receives leftover minor units.
@@ -123,7 +137,8 @@ maxAge 1d` requires a recent completed provider check.
123
137
  `moves reserve amount from payer to payee capture receipt` reserves a transfer;
124
138
  `moves post self.receipt` posts it and `moves void self.receipt` releases it.
125
139
  `moves amount from payer shares shares` expands a declared split. Optional `fee`,
126
- `capture` and `key` modifiers follow a move in that order. Repeated clauses keep
140
+ `capture`, `key` and `boundary` modifiers describe a move. `boundary` applies
141
+ only to reservations. Fees settle with create moves, never reservations. Repeated clauses keep
127
142
  their declaration order. The JSON-like clause form remains accepted and lowers
128
143
  to the same [UDL clauses](../../udl/spec/README.md).
129
144
  They cannot add kernel instructions. `at(list, position)` reads a dated list;
@@ -140,16 +155,16 @@ and a correction. No artifact is returned with diagnostics.
140
155
  activation. The company chooses where funding goes and when profit becomes earned.
141
156
  All choices use accounts, calculations and moves in the same two books.
142
157
 
143
- | Tunable | Default | Selected behavior |
144
- | ---------------------------------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
145
- | `disburse_to` | `funds` | `funds` pays down payment and principal into the hold and confirms it. `borrower` credits principal to the borrower's balance. |
146
- | `profit_earned` | `on_payment` | `on_payment` collects unearned profit on payment. `by_schedule` moves unearned claims to earned claims at each slice's date. `at_disbursement` makes that move at activation. Cash arrives only on payment. |
147
- | `apply` | `fines_profit_principal` | Collect assessed fines by overdue date, then evidenced costs, then each slice in position order, profit before principal. `principal_profit` collects each slice in position order, principal before profit. `pro_rata` uses the slice's fixed principal:profit ratio, caps each side by its collectible balance, and assigns the remainder without stranding cash. |
148
- | `payoff_rebate` | `100%` | Reverse this share of unearned profit to borrower debt. Collect the remainder and all earned profit with outstanding principal. |
149
- | `late_charge.fines_to` | `programOperator` | Receive fine cash and fund its refunds. |
150
- | `late_charge.costs_to` | `programOperator` | Receive evidenced recovery cash and fund its refunds. |
151
- | `collections.case.fee` | `20%` | After payment succeeds, transfer this share of the payment amount from plan capital to the agency. |
152
- | `lending.distribution.residual_to` | `programOperator` | Receive the cash remainder and own the claim loss remainder after weighted distribution. |
158
+ | Tunable | Default | Selected behavior |
159
+ | ---------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
160
+ | `disburse_to` | `funds` | `funds` pays down payment and principal into the hold and confirms it. `borrower` credits principal to the borrower's balance. |
161
+ | `profit_earned` | `on_payment` | `on_payment` collects unearned profit on payment. `by_schedule` moves unearned claims to earned claims at each slice's date. `at_disbursement` makes that move at activation. Cash arrives only on payment. |
162
+ | `apply` | `fines_profit_principal` | Visit each slice in position order; within that slice collect assessed fines by overdue date, evidenced costs, profit, then principal. `principal_profit` collects each slice in position order, principal before profit. `pro_rata` uses the slice's fixed principal:profit ratio, caps each side by its collectible balance, and assigns the remainder without stranding cash. |
163
+ | `payoff_rebate` | `100%` | Reverse this share of unearned profit to borrower debt. Collect the remainder and all earned profit with outstanding principal. |
164
+ | `late_charge.fines_to` | `programOperator` | Receive fine cash and fund its refunds. |
165
+ | `late_charge.costs_to` | `programOperator` | Receive evidenced recovery cash and fund its refunds. |
166
+ | `collections.case.fee` | `20%` | After payment succeeds, transfer this share of the payment amount from plan capital to the agency. |
167
+ | `lending.distribution.residual_to` | `programOperator` | Receive the cash remainder and own the claim loss remainder after weighted distribution. |
153
168
 
154
169
  Each slice owns separate unearned and earned claim accounts. Scheduled recognition
155
170
  uses a dated child record so a partial payment cannot disable the maturity clock.
@@ -159,7 +174,8 @@ costs stay assessed until their receivable reaches zero. Immutable receipts let
159
174
  a refund restore only what that payment collected. The plan's payment receipt
160
175
  records actual principal and profit after all ordered collections finish.
161
176
  Write-off moves principal and earned profit claims to loss and reverses unearned
162
- profit to debt. There is no provision policy or write-off tunable.
177
+ profit to debt. `write_off_after` sets how long a slice must be overdue before a capital-authorized
178
+ write-off request can apply. There is no provision policy.
163
179
 
164
180
  ## Header branches and bounds
165
181
 
@@ -173,9 +189,7 @@ when disburse_to is borrower {
173
189
 
174
190
  `when <reference tunable> has <field>` includes its clauses only when the bound
175
191
  object declares that field. The compiler checks the object's declared shape,
176
- regardless of declaration order, and emits no runtime branch. Financing uses
177
- this to commit marketplace orders for order-backed holds while plain money
178
- holds need no order relation.
192
+ regardless of declaration order, and emits no runtime branch.
179
193
 
180
194
  `when <enum tunable> is <value>` accepts requirements, calculations, moves and
181
195
  invocations, including nested branches. The compiler emits only the selected
@@ -253,7 +267,7 @@ adapters are bound, participants have funds, or the flow can finish.
253
267
  | `financing.limits` | `per_borrower` caps outstanding principal. Both borrower and portfolio limits must be approved before disbursement; attaching them does not approve them. |
254
268
  | `lending.round` | The target is the linked plan's principal. Closing requires commitments and held funds to equal that amount. |
255
269
  | `lending.distribution` | Cash distribution needs an eligible recorded settlement, `prepare_cash`, one share record per funded commitment, then `distribute_cash`. Attaching it moves nothing. |
256
- | `insurance.cover.slice` | `commission` is calculated, but `collect` sends the whole premium to `programOperator`; that action makes no broker commission transfer. |
270
+ | `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. |
257
271
  | `travel.booking` | From `deposit_paid` or `paid`, early cancellation returns the full held balance, middle returns held balance minus deposit, and late has no buyer refund transfer. State, time and balance requirements still apply. A deposit-only early cancellation refunds the deposit; a deposit-only middle cancellation refunds zero. |
258
272
  | `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 positions. |
259
273
  | `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. |
@@ -11,9 +11,9 @@ object device "Insured device" {
11
11
  payer: owner, payee: operator
12
12
  expose pay as pay_for_device
13
13
  }
14
- // Bind device_insurer to an ADL adapter before activating cover.
14
+ // Bind device_insurer to an ADL adapter before creating cover. Premiums net of commission go to its account.
15
15
  attach protection = insurance.cover {
16
- holder: owner, adapter: "device_insurer", covers: purchase
16
+ holder: owner, adapter: "device_insurer", commission: 10%, covers: purchase
17
17
  expose activate as activate_cover
18
18
  expose cancel as cancel_cover
19
19
  }
@@ -11,9 +11,9 @@ object holiday "Holiday booking" {
11
11
  price: 5000 SAR, supplier_cost: 3500 SAR, departure: 2027-07-01
12
12
  expose publish as publish_trip
13
13
  }
14
- // Bind travel_insurer to an ADL adapter before activating cover.
14
+ // Bind travel_insurer to an ADL adapter before creating cover. Premiums net of commission go to its account.
15
15
  attach protection = insurance.cover {
16
- holder: actor, adapter: "travel_insurer", covers: reservation
16
+ holder: actor, adapter: "travel_insurer", commission: 10%, covers: reservation
17
17
  expose activate as activate_cover
18
18
  }
19
19
  attach reservation = travel.booking {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperscale0/hsx",
3
- "version": "5.0.1",
3
+ "version": "5.2.0",
4
4
  "description": "The strictly typed HSX language: compile total financial programs into canonical UDL.",
5
5
  "keywords": [
6
6
  "hsx",
@@ -86,7 +86,7 @@
86
86
  },
87
87
  "dependencies": {
88
88
  "@hyperscale0/adl": "1.0.0-beta.8",
89
- "@hyperscale0/udl": "4.2.1",
89
+ "@hyperscale0/udl": "4.4.0",
90
90
  "fast-sha256": "1.3.0"
91
91
  },
92
92
  "devDependencies": {
@@ -98,5 +98,5 @@
98
98
  "Amir Ayub",
99
99
  "Sara AlBakaawi"
100
100
  ],
101
- "gitHead": "2ba5fb5f926f8f78ac3a5f2531c0b5b9266de9ac"
101
+ "gitHead": "f8eb5c255acd23edba069d63962611b81280c85d"
102
102
  }
package/src/compile.ts CHANGED
@@ -643,6 +643,8 @@ export function compile(
643
643
  renames: Map<string, string>;
644
644
  exposed: Map<string, string>;
645
645
  parties: Record<string, AttachmentPartyBinding>;
646
+ attachments: UdlObjectAttachment[];
647
+ child?: boolean;
646
648
  },
647
649
  familyDeclaration?: {
648
650
  module: string;
@@ -1709,7 +1711,16 @@ export function compile(
1709
1711
  "account needs an owner, optional book, mode and key",
1710
1712
  "write account(buyer, claim, contra)",
1711
1713
  );
1712
- f.owner = text(resolve(t.args[0]!));
1714
+ const owner = t.args[0]!;
1715
+ if (owner.kind === "call" && owner.name === "adapter") {
1716
+ if (owner.args.length !== 1)
1717
+ fail(
1718
+ owner,
1719
+ "adapter needs one binding",
1720
+ 'write account(adapter(binding), cash, "premium")',
1721
+ );
1722
+ f.owner = { adapter: text(resolve(owner.args[0]!)) };
1723
+ } else f.owner = text(resolve(owner));
1713
1724
  f.book = t.args[1] ? text(t.args[1]) : "cash";
1714
1725
  if (t.args[2]) {
1715
1726
  const mode = text(t.args[2]);
@@ -2549,7 +2560,7 @@ export function compile(
2549
2560
  }
2550
2561
  } else (a as unknown as Record<string, unknown>)[key] = data(expr);
2551
2562
  }
2552
- if (attachmentInfo) {
2563
+ if (attachmentInfo && !attachmentInfo.child) {
2553
2564
  if (attachmentInfo.exposed.has(name)) {
2554
2565
  a.publicAction = attachmentInfo.exposed.get(name)!;
2555
2566
  } else {
@@ -2619,6 +2630,21 @@ export function compile(
2619
2630
  }
2620
2631
  document.instruments.push(inst);
2621
2632
  for (const [key, definition] of records) {
2633
+ const childAttachment = attachmentInfo
2634
+ ? {
2635
+ ...attachmentInfo,
2636
+ attachmentName: `${attachmentInfo.attachmentName}_${key}`,
2637
+ child: true,
2638
+ exposed: new Map<string, string>(),
2639
+ }
2640
+ : undefined;
2641
+ if (childAttachment)
2642
+ childAttachment.attachments.push({
2643
+ name: childAttachment.attachmentName,
2644
+ parent: attachmentInfo!.attachmentName,
2645
+ instrument: `${id}_${key}`,
2646
+ parties: childAttachment.parties,
2647
+ });
2622
2648
  const child: InstrumentDecl = {
2623
2649
  kind: "instrument",
2624
2650
  name: key,
@@ -2649,9 +2675,7 @@ export function compile(
2649
2675
  ["parent", { kind: "name", value: id, span: origin } as Expr],
2650
2676
  ]),
2651
2677
  enums,
2652
- attachmentInfo
2653
- ? { ...attachmentInfo, exposed: new Map() }
2654
- : undefined,
2678
+ childAttachment,
2655
2679
  childFamily,
2656
2680
  );
2657
2681
  }
@@ -2746,6 +2770,7 @@ export function compile(
2746
2770
  };
2747
2771
 
2748
2772
  const templateFamily = resolveFamily(targetTemplate, entry, false);
2773
+ const firstAttachedInstrument = document.instruments.length;
2749
2774
  try {
2750
2775
  addInstrument(
2751
2776
  template,
@@ -2760,6 +2785,7 @@ export function compile(
2760
2785
  renames,
2761
2786
  exposed,
2762
2787
  parties,
2788
+ attachments,
2763
2789
  },
2764
2790
  templateFamily ? { ...templateFamily } : undefined,
2765
2791
  );
@@ -2769,17 +2795,24 @@ export function compile(
2769
2795
  continue;
2770
2796
  }
2771
2797
  const attachedInst = document.instruments.find((i) => i.id === instId);
2772
- if (attachedInst?.actions.create) {
2798
+ for (const createdInst of document.instruments
2799
+ .slice(firstAttachedInstrument)
2800
+ .filter(
2801
+ (instrument) =>
2802
+ instrument.id === instId ||
2803
+ instrument.actions.create?.publicAction,
2804
+ )) {
2805
+ if (!createdInst.actions.create) continue;
2773
2806
  const owned = new Set(
2774
- attachedInst.calculate.map((node) => node.target),
2807
+ createdInst.calculate.map((node) => node.target),
2775
2808
  );
2776
- for (const action of Object.values(attachedInst.actions)) {
2809
+ for (const action of Object.values(createdInst.actions)) {
2777
2810
  for (const node of action.calculate ?? []) owned.add(node.target);
2778
2811
  for (const move of action.moves)
2779
2812
  if ("capture" in move && move.capture) owned.add(move.capture);
2780
2813
  }
2781
- const create = attachedInst.actions.create;
2782
- for (const field of attachedInst.fields) {
2814
+ const create = createdInst.actions.create;
2815
+ for (const field of createdInst.fields) {
2783
2816
  if (
2784
2817
  field.type === "account" ||
2785
2818
  (field.type === "ref" && field.targetKind === "instrument") ||
@@ -2788,7 +2821,8 @@ export function compile(
2788
2821
  field.targetKind === "instrument") ||
2789
2822
  field.optional ||
2790
2823
  "value" in field ||
2791
- owned.has(field.name)
2824
+ owned.has(field.name) ||
2825
+ create.input.some((input) => input.name === field.name)
2792
2826
  )
2793
2827
  continue;
2794
2828
  create.subject ??= { requirements: [], adapters: [] };
@@ -2800,7 +2834,7 @@ export function compile(
2800
2834
  failWithCode(
2801
2835
  entry,
2802
2836
  "subject_field_conflict",
2803
- `${instId}.create.subject.${field.name} conflicts with its instrument field`,
2837
+ `${createdInst.id}.create.subject.${field.name} conflicts with its instrument field`,
2804
2838
  "use the instrument field's type and constraints",
2805
2839
  );
2806
2840
  continue;
@@ -2814,7 +2848,7 @@ export function compile(
2814
2848
  requirementOrigins.set(requirement, {
2815
2849
  source: declarationSources.get(template) ?? "program",
2816
2850
  span: entry.span,
2817
- message: `${instId}.create.fields.${field.name}`,
2851
+ message: `${createdInst.id}.create.fields.${field.name}`,
2818
2852
  });
2819
2853
  }
2820
2854
  }
@@ -2913,16 +2947,15 @@ export function compile(
2913
2947
  .sort((a, b) => a.span.start - b.span.start)
2914
2948
  .map((d) => diagnostic(d, "check")),
2915
2949
  };
2916
- // Propagate mandatory invoked action requirements
2950
+ // Propagate invoked requirements with the conditions on each invocation path.
2917
2951
  let changedInvocations = true;
2918
2952
  let invocationIterations = 0;
2919
2953
  while (changedInvocations && invocationIterations < 32) {
2920
2954
  changedInvocations = false;
2921
2955
  invocationIterations++;
2922
2956
  for (const inst of document.instruments) {
2923
- for (const action of Object.values(inst.actions)) {
2957
+ for (const [actionName, action] of Object.entries(inst.actions)) {
2924
2958
  for (const call of action.invoke ?? []) {
2925
- if (call.guard) continue;
2926
2959
  let targetIds: string[] = [];
2927
2960
  if ("instrument" in call) {
2928
2961
  targetIds = [call.instrument];
@@ -2962,7 +2995,9 @@ export function compile(
2962
2995
  if (!action.subject) {
2963
2996
  action.subject = { requirements: [], adapters: [] };
2964
2997
  }
2965
- for (const adapter of targetAction.subject.adapters) {
2998
+ for (const adapter of call.guard
2999
+ ? []
3000
+ : targetAction.subject.adapters) {
2966
3001
  if (
2967
3002
  !action.subject.adapters.some(
2968
3003
  (existing) => existing.binding === adapter.binding,
@@ -2979,9 +3014,65 @@ export function compile(
2979
3014
  (cr) =>
2980
3015
  (cr.objectField ?? cr.field.name) === targetObjFieldName,
2981
3016
  );
3017
+ const guardValue = (value: UdlValue): UdlValue => {
3018
+ if (
3019
+ !("field" in value) ||
3020
+ !value.field.startsWith("subject.")
3021
+ )
3022
+ return value;
3023
+ const requirement = action.subject?.requirements.find(
3024
+ (item) => item.field.name === value.field.slice(8),
3025
+ );
3026
+ return {
3027
+ field: `subject.${requirement?.objectField ?? requirement?.field.name ?? value.field.slice(8)}`,
3028
+ };
3029
+ };
3030
+ const guardType =
3031
+ call.guard &&
3032
+ [call.guard.left, call.guard.right]
3033
+ .flatMap((value) =>
3034
+ "field" in value
3035
+ ? [
3036
+ resolveField(
3037
+ document,
3038
+ inst,
3039
+ value.field,
3040
+ action.input,
3041
+ action,
3042
+ )?.type,
3043
+ ]
3044
+ : [],
3045
+ )
3046
+ .find((type) => type === "money" || type === "date");
3047
+ const condition:
3048
+ | NonNullable<UdlSubjectRequirement["when"]>[number][number]
3049
+ | undefined = call.guard
3050
+ ? {
3051
+ instrument: inst.id,
3052
+ action: actionName,
3053
+ guard: {
3054
+ ...call.guard,
3055
+ left: guardValue(call.guard.left),
3056
+ right: guardValue(call.guard.right),
3057
+ },
3058
+ ...(guardType === "money" || guardType === "date"
3059
+ ? { valueType: guardType }
3060
+ : {}),
3061
+ }
3062
+ : undefined;
3063
+ const when = condition
3064
+ ? (targetReq.when ?? [[]]).map((path) => [
3065
+ condition,
3066
+ ...path.filter(
3067
+ (item) =>
3068
+ canonicalJson(item) !== canonicalJson(condition),
3069
+ ),
3070
+ ])
3071
+ : targetReq.when;
2982
3072
  if (!existing) {
2983
- const inherited = {
3073
+ const inherited: UdlSubjectRequirement = {
2984
3074
  field: { ...targetReq.field, name: targetObjFieldName },
3075
+ ...(when ? { when: when.map((path) => [...path]) } : {}),
2985
3076
  };
2986
3077
  action.subject.requirements.push(inherited);
2987
3078
  requirementOrigins.set(
@@ -3002,6 +3093,23 @@ export function compile(
3002
3093
  source: first.source,
3003
3094
  related: [first, second],
3004
3095
  });
3096
+ } else if (existing.when) {
3097
+ if (!when) {
3098
+ delete existing.when;
3099
+ changedInvocations = true;
3100
+ } else {
3101
+ for (const path of when) {
3102
+ if (
3103
+ !existing.when.some(
3104
+ (known) =>
3105
+ canonicalJson(known) === canonicalJson(path),
3106
+ )
3107
+ ) {
3108
+ existing.when.push(path);
3109
+ changedInvocations = true;
3110
+ }
3111
+ }
3112
+ }
3005
3113
  }
3006
3114
  }
3007
3115
  }
@@ -5,7 +5,7 @@ export const examples: HsxExample[] = [
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\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 { borrower: owner, per_borrower: 100000 SAR }\n attach budget = financing.portfolio_limit { limit: 2000000 SAR }\n attach loan = financing.installments {\n borrower: owner, capital: operator\n months: 12, profit: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_loan\n expose disburse as release_loan\n }\n attach recovery = collections.case {\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 case: recovery, agency: agency\n }\n}\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 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 }\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 borrower: owner, per_borrower: 12000 SAR\n expose approve as approve_student\n }\n attach budget = financing.portfolio_limit {\n limit: 500000 SAR\n expose approve as approve_budget\n }\n attach tuition = financing.installments {\n borrower: owner, capital: operator\n months: 3, profit: 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"},
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\nparty inspector: staff role claims\n\nobject device \"Insured device\" {\n fields { model: text, serialNumber: text, purchaseDate: date }\n columns: [model, serialNumber, purchaseDate]\n attach purchase = money.transfer {\n payer: owner, payee: operator\n expose pay as pay_for_device\n }\n // Bind device_insurer to an ADL adapter before activating cover.\n attach protection = insurance.cover {\n holder: owner, adapter: \"device_insurer\", covers: purchase\n expose activate as activate_cover\n expose cancel as cancel_cover\n }\n attach claim = insurance.claim {\n cover: protection, inspector: inspector\n expose approve as approve_claim\n expose deny as deny_claim\n expose pay as pay_claim\n }\n}\n"},
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\nparty inspector: staff role claims\n\nobject device \"Insured device\" {\n fields { model: text, serialNumber: text, purchaseDate: date }\n columns: [model, serialNumber, purchaseDate]\n attach purchase = money.transfer {\n payer: owner, payee: operator\n expose pay as pay_for_device\n }\n // Bind device_insurer to an ADL adapter before creating cover. Premiums net of commission go to its account.\n attach protection = insurance.cover {\n holder: owner, adapter: \"device_insurer\", commission: 10%, covers: purchase\n expose activate as activate_cover\n expose cancel as cancel_cover\n }\n attach claim = insurance.claim {\n cover: protection, inspector: inspector\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. Repayment and distribution-share child actions still need an execution path; this is not a complete public repayment flow.","headers":["lending","financing","wallet"],"source":"// A lending composition with limit approvals, wallet commitments and cash distribution transitions.\n// Repayment and distribution-share child actions still need an execution path; this is not a complete public repayment flow.\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 borrower: owner, per_borrower: 100000 SAR\n expose approve as approve_borrower\n }\n attach budget = financing.portfolio_limit {\n limit: 2000000 SAR\n expose approve as approve_budget\n }\n attach loan = financing.installments {\n borrower: owner, capital: operator\n months: 12, profit: 6%, disburse_to: borrower\n limits: allowance, portfolio: budget\n expose sign as sign_loan\n expose disburse as release_loan\n }\n attach investor_balance = wallet.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 round: funding, receipt: loan.settlement\n expose prepare_cash as prepare_returns\n expose distribute_cash as distribute_returns\n }\n}\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 { payer: actor, payee: owner, expose fund as sell }\n attach limits = financing.limits { borrower: actor, per_borrower: 60000 SAR }\n attach ceiling = financing.portfolio_limit { limit: 1500000 SAR }\n attach plan = financing.installments {\n borrower: actor, capital: operator, share: 25%\n months: 3, profit: 2.5%, funds: sale, limits: limits, portfolio: ceiling\n expose create as finance\n }\n}\n"},
11
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 seller: owner\n expose publish as publish_offer\n expose withdraw as withdraw_offer\n }\n attach order = marketplace.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 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 }\n}\n"},
@@ -14,6 +14,6 @@ export const examples: HsxExample[] = [
14
14
  {"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 borrower: owner, per_borrower: 200000 SAR\n expose approve as approve_business\n }\n attach budget = financing.portfolio_limit {\n limit: 5000000 SAR\n expose approve as approve_budget\n }\n attach loan = financing.installments {\n borrower: owner, capital: operator\n months: 12, profit: 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 { on: loan }\n}\n"},
15
15
  {"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 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 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"},
16
16
  {"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 { payer: actor, payee: operator, expose fund as purchase }\n attach limits = financing.limits { borrower: actor, per_borrower: 250000 SAR }\n attach ceiling = financing.portfolio_limit { limit: 10000000 SAR }\n attach plan = financing.installments {\n borrower: actor, capital: operator\n months: 36, profit: 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 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 { on: plan, operator: operator, before_days: 3, overdue_days: 1 }\n}\n"},
17
- {"id":"travel","title":"Travel agency","summary":"A travel agency takes deposits for an insured holiday and pays its supplier after confirmation.","headers":["travel","insurance"],"source":"// A travel agency takes deposits for an insured holiday and pays its supplier after confirmation.\nprogram travel_agency \"Travel agency\"\nuse travel\nuse insurance\nparty hotel: business\n\nobject holiday \"Holiday booking\" {\n fields { destination: text, leadTraveller: text }\n columns: [destination, leadTraveller]\n attach trip = travel.package {\n price: 5000 SAR, supplier_cost: 3500 SAR, departure: 2027-07-01\n expose publish as publish_trip\n }\n // Bind travel_insurer to an ADL adapter before activating cover.\n attach protection = insurance.cover {\n holder: actor, adapter: \"travel_insurer\", covers: reservation\n expose activate as activate_cover\n }\n attach reservation = travel.booking {\n package: trip, buyer: actor, supplier: hotel, cover: protection\n expose deposit as pay_deposit\n expose balance as pay_balance\n expose confirm as confirm_completion\n expose cancel_early as cancel_booking\n }\n}\n"},
17
+ {"id":"travel","title":"Travel agency","summary":"A travel agency takes deposits for an insured holiday and pays its supplier after confirmation.","headers":["travel","insurance"],"source":"// A travel agency takes deposits for an insured holiday and pays its supplier after confirmation.\nprogram travel_agency \"Travel agency\"\nuse travel\nuse insurance\nparty hotel: business\n\nobject holiday \"Holiday booking\" {\n fields { destination: text, leadTraveller: text }\n columns: [destination, leadTraveller]\n attach trip = travel.package {\n price: 5000 SAR, supplier_cost: 3500 SAR, departure: 2027-07-01\n expose publish as publish_trip\n }\n // Bind travel_insurer to an ADL adapter before creating cover. Premiums net of commission go to its account.\n attach protection = insurance.cover {\n holder: actor, adapter: \"travel_insurer\", commission: 10%, covers: reservation\n expose activate as activate_cover\n }\n attach reservation = travel.booking {\n package: trip, buyer: actor, supplier: hotel, cover: protection\n expose deposit as pay_deposit\n expose balance as pay_balance\n expose confirm as confirm_completion\n expose cancel_early as cancel_booking\n }\n}\n"},
18
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 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 wallet: balance, payee: operator\n expose reserve as reserve_visit\n expose pay as charge_visit\n expose cancel as cancel_visit\n }\n}\n"},
19
19
  ];