@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.
- package/CHANGELOG.md +22 -0
- package/dist/src/compile.d.ts.map +1 -1
- package/dist/src/compile.js +98 -18
- package/dist/src/compile.js.map +1 -1
- package/dist/src/examples-bundle.js +2 -2
- package/dist/src/examples-bundle.js.map +1 -1
- package/dist/src/std-bundle.js +6 -6
- package/dist/src/std-bundle.js.map +1 -1
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/docs/README.md +32 -18
- package/examples/insurance.hsx +2 -2
- package/examples/travel.hsx +2 -2
- package/package.json +3 -3
- package/src/compile.ts +126 -18
- package/src/examples-bundle.ts +2 -2
- package/src/std-bundle.ts +6 -6
- package/src/version.ts +1 -1
- package/std/cards.hsx +2 -0
- package/std/financing.hsx +1 -1
- package/std/insurance.hsx +9 -2
- package/std/lending.hsx +3 -2
- package/std/money.hsx +1 -0
- package/std/reporting.hsx +32 -4
|
@@ -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,
|
|
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"}
|
package/dist/src/version.d.ts
CHANGED
package/dist/src/version.js
CHANGED
package/docs/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# HSX
|
|
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
|
-
|
|
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 `
|
|
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` |
|
|
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.
|
|
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.
|
|
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` | `
|
|
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. |
|
package/examples/insurance.hsx
CHANGED
|
@@ -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
|
|
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
|
}
|
package/examples/travel.hsx
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2807
|
+
createdInst.calculate.map((node) => node.target),
|
|
2775
2808
|
);
|
|
2776
|
-
for (const action of Object.values(
|
|
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 =
|
|
2782
|
-
for (const field of
|
|
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
|
-
`${
|
|
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: `${
|
|
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
|
|
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.
|
|
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
|
|
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
|
}
|
package/src/examples-bundle.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
];
|