@hyperscale0/hsx 5.0.0 → 5.1.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,g7FAAg7F,CAAC;IACn8F,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.0";
1
+ export declare const HSX_VERSION = "5.1.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.0";
1
+ export const HSX_VERSION = "5.1.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.
@@ -70,7 +70,9 @@ 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
72
  `account(buyer, claim, contra, "debt")` declares the borrower's claim contra account.
73
- `account(seller, cash, external, "bank")` binds the executor-managed bank destination.
73
+ Provider confirmation belongs on a reserved move with `boundary adapter`,
74
+ followed by instruction-bound evidence and a post or void. External account
75
+ mode is not supported.
74
76
  Outstanding debt is an account balance. The library pairs cash repayment with
75
77
  claim reduction and represents receipts as immutable child records. Cash and loss
76
78
  shares round down; the declared residual account receives leftover minor units.
@@ -123,7 +125,8 @@ maxAge 1d` requires a recent completed provider check.
123
125
  `moves reserve amount from payer to payee capture receipt` reserves a transfer;
124
126
  `moves post self.receipt` posts it and `moves void self.receipt` releases it.
125
127
  `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
128
+ `capture`, `key` and `boundary` modifiers describe a move. `boundary` applies
129
+ only to reservations. Fees settle with create moves, never reservations. Repeated clauses keep
127
130
  their declaration order. The JSON-like clause form remains accepted and lowers
128
131
  to the same [UDL clauses](../../udl/spec/README.md).
129
132
  They cannot add kernel instructions. `at(list, position)` reads a dated list;
@@ -140,16 +143,16 @@ and a correction. No artifact is returned with diagnostics.
140
143
  activation. The company chooses where funding goes and when profit becomes earned.
141
144
  All choices use accounts, calculations and moves in the same two books.
142
145
 
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. |
146
+ | Tunable | Default | Selected behavior |
147
+ | ---------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
148
+ | `disburse_to` | `funds` | `funds` pays down payment and principal into the hold and confirms it. `borrower` credits principal to the borrower's balance. |
149
+ | `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. |
150
+ | `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. |
151
+ | `payoff_rebate` | `100%` | Reverse this share of unearned profit to borrower debt. Collect the remainder and all earned profit with outstanding principal. |
152
+ | `late_charge.fines_to` | `programOperator` | Receive fine cash and fund its refunds. |
153
+ | `late_charge.costs_to` | `programOperator` | Receive evidenced recovery cash and fund its refunds. |
154
+ | `collections.case.fee` | `20%` | After payment succeeds, transfer this share of the payment amount from plan capital to the agency. |
155
+ | `lending.distribution.residual_to` | `programOperator` | Receive the cash remainder and own the claim loss remainder after weighted distribution. |
153
156
 
154
157
  Each slice owns separate unearned and earned claim accounts. Scheduled recognition
155
158
  uses a dated child record so a partial payment cannot disable the maturity clock.
@@ -159,7 +162,8 @@ costs stay assessed until their receivable reaches zero. Immutable receipts let
159
162
  a refund restore only what that payment collected. The plan's payment receipt
160
163
  records actual principal and profit after all ordered collections finish.
161
164
  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.
165
+ profit to debt. `write_off_after` sets how long a slice must be overdue before a capital-authorized
166
+ write-off request can apply. There is no provision policy.
163
167
 
164
168
  ## Header branches and bounds
165
169
 
@@ -173,9 +177,7 @@ when disburse_to is borrower {
173
177
 
174
178
  `when <reference tunable> has <field>` includes its clauses only when the bound
175
179
  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.
180
+ regardless of declaration order, and emits no runtime branch.
179
181
 
180
182
  `when <enum tunable> is <value>` accepts requirements, calculations, moves and
181
183
  invocations, including nested branches. The compiler emits only the selected
@@ -253,7 +255,7 @@ adapters are bound, participants have funds, or the flow can finish.
253
255
  | `financing.limits` | `per_borrower` caps outstanding principal. Both borrower and portfolio limits must be approved before disbursement; attaching them does not approve them. |
254
256
  | `lending.round` | The target is the linked plan's principal. Closing requires commitments and held funds to equal that amount. |
255
257
  | `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. |
258
+ | `insurance.cover.slice` | `collect` credits the whole premium to the tenant. The required insurer payout of premium minus commission is blocked on generic adapter-owned account binding. This header does not yet implement insurer settlement. |
257
259
  | `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
260
  | `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
261
  | `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. |
@@ -266,3 +268,9 @@ commitment creation, and cash prepare/distribute actions. Its repayment and shar
266
268
  child actions still lack a public execution path. It is a composition example,
267
269
  not a complete public repayment flow. Callers still need dates, agreement inputs,
268
270
  funded wallets, eligible settlements and the distribution's share records.
271
+
272
+ Insurance providers are adapters, never party declarations. The tenant must retain
273
+ commission and pay the insurer the remaining premium through a confirmed move.
274
+ The current account ABI cannot bind an adapter-owned destination. Do not treat
275
+ `insurance.cover` or `insurance.claim` as a complete insurer integration; claims
276
+ currently reserve tenant funds.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperscale0/hsx",
3
- "version": "5.0.0",
3
+ "version": "5.1.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.0",
89
+ "@hyperscale0/udl": "4.3.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": "2c4281aeb2cc0a10e13a91e8d67ae7abf608680f"
101
+ "gitHead": "5bb82154896feeb7fa002d3bff439a6f90ed91e1"
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;
@@ -2549,7 +2551,7 @@ export function compile(
2549
2551
  }
2550
2552
  } else (a as unknown as Record<string, unknown>)[key] = data(expr);
2551
2553
  }
2552
- if (attachmentInfo) {
2554
+ if (attachmentInfo && !attachmentInfo.child) {
2553
2555
  if (attachmentInfo.exposed.has(name)) {
2554
2556
  a.publicAction = attachmentInfo.exposed.get(name)!;
2555
2557
  } else {
@@ -2619,6 +2621,21 @@ export function compile(
2619
2621
  }
2620
2622
  document.instruments.push(inst);
2621
2623
  for (const [key, definition] of records) {
2624
+ const childAttachment = attachmentInfo
2625
+ ? {
2626
+ ...attachmentInfo,
2627
+ attachmentName: `${attachmentInfo.attachmentName}_${key}`,
2628
+ child: true,
2629
+ exposed: new Map<string, string>(),
2630
+ }
2631
+ : undefined;
2632
+ if (childAttachment)
2633
+ childAttachment.attachments.push({
2634
+ name: childAttachment.attachmentName,
2635
+ parent: attachmentInfo!.attachmentName,
2636
+ instrument: `${id}_${key}`,
2637
+ parties: childAttachment.parties,
2638
+ });
2622
2639
  const child: InstrumentDecl = {
2623
2640
  kind: "instrument",
2624
2641
  name: key,
@@ -2649,9 +2666,7 @@ export function compile(
2649
2666
  ["parent", { kind: "name", value: id, span: origin } as Expr],
2650
2667
  ]),
2651
2668
  enums,
2652
- attachmentInfo
2653
- ? { ...attachmentInfo, exposed: new Map() }
2654
- : undefined,
2669
+ childAttachment,
2655
2670
  childFamily,
2656
2671
  );
2657
2672
  }
@@ -2746,6 +2761,7 @@ export function compile(
2746
2761
  };
2747
2762
 
2748
2763
  const templateFamily = resolveFamily(targetTemplate, entry, false);
2764
+ const firstAttachedInstrument = document.instruments.length;
2749
2765
  try {
2750
2766
  addInstrument(
2751
2767
  template,
@@ -2760,6 +2776,7 @@ export function compile(
2760
2776
  renames,
2761
2777
  exposed,
2762
2778
  parties,
2779
+ attachments,
2763
2780
  },
2764
2781
  templateFamily ? { ...templateFamily } : undefined,
2765
2782
  );
@@ -2769,17 +2786,24 @@ export function compile(
2769
2786
  continue;
2770
2787
  }
2771
2788
  const attachedInst = document.instruments.find((i) => i.id === instId);
2772
- if (attachedInst?.actions.create) {
2789
+ for (const createdInst of document.instruments
2790
+ .slice(firstAttachedInstrument)
2791
+ .filter(
2792
+ (instrument) =>
2793
+ instrument.id === instId ||
2794
+ instrument.actions.create?.publicAction,
2795
+ )) {
2796
+ if (!createdInst.actions.create) continue;
2773
2797
  const owned = new Set(
2774
- attachedInst.calculate.map((node) => node.target),
2798
+ createdInst.calculate.map((node) => node.target),
2775
2799
  );
2776
- for (const action of Object.values(attachedInst.actions)) {
2800
+ for (const action of Object.values(createdInst.actions)) {
2777
2801
  for (const node of action.calculate ?? []) owned.add(node.target);
2778
2802
  for (const move of action.moves)
2779
2803
  if ("capture" in move && move.capture) owned.add(move.capture);
2780
2804
  }
2781
- const create = attachedInst.actions.create;
2782
- for (const field of attachedInst.fields) {
2805
+ const create = createdInst.actions.create;
2806
+ for (const field of createdInst.fields) {
2783
2807
  if (
2784
2808
  field.type === "account" ||
2785
2809
  (field.type === "ref" && field.targetKind === "instrument") ||
@@ -2788,7 +2812,8 @@ export function compile(
2788
2812
  field.targetKind === "instrument") ||
2789
2813
  field.optional ||
2790
2814
  "value" in field ||
2791
- owned.has(field.name)
2815
+ owned.has(field.name) ||
2816
+ create.input.some((input) => input.name === field.name)
2792
2817
  )
2793
2818
  continue;
2794
2819
  create.subject ??= { requirements: [], adapters: [] };
@@ -2800,7 +2825,7 @@ export function compile(
2800
2825
  failWithCode(
2801
2826
  entry,
2802
2827
  "subject_field_conflict",
2803
- `${instId}.create.subject.${field.name} conflicts with its instrument field`,
2828
+ `${createdInst.id}.create.subject.${field.name} conflicts with its instrument field`,
2804
2829
  "use the instrument field's type and constraints",
2805
2830
  );
2806
2831
  continue;
@@ -2814,7 +2839,7 @@ export function compile(
2814
2839
  requirementOrigins.set(requirement, {
2815
2840
  source: declarationSources.get(template) ?? "program",
2816
2841
  span: entry.span,
2817
- message: `${instId}.create.fields.${field.name}`,
2842
+ message: `${createdInst.id}.create.fields.${field.name}`,
2818
2843
  });
2819
2844
  }
2820
2845
  }
@@ -2913,16 +2938,15 @@ export function compile(
2913
2938
  .sort((a, b) => a.span.start - b.span.start)
2914
2939
  .map((d) => diagnostic(d, "check")),
2915
2940
  };
2916
- // Propagate mandatory invoked action requirements
2941
+ // Propagate invoked requirements with the conditions on each invocation path.
2917
2942
  let changedInvocations = true;
2918
2943
  let invocationIterations = 0;
2919
2944
  while (changedInvocations && invocationIterations < 32) {
2920
2945
  changedInvocations = false;
2921
2946
  invocationIterations++;
2922
2947
  for (const inst of document.instruments) {
2923
- for (const action of Object.values(inst.actions)) {
2948
+ for (const [actionName, action] of Object.entries(inst.actions)) {
2924
2949
  for (const call of action.invoke ?? []) {
2925
- if (call.guard) continue;
2926
2950
  let targetIds: string[] = [];
2927
2951
  if ("instrument" in call) {
2928
2952
  targetIds = [call.instrument];
@@ -2962,7 +2986,9 @@ export function compile(
2962
2986
  if (!action.subject) {
2963
2987
  action.subject = { requirements: [], adapters: [] };
2964
2988
  }
2965
- for (const adapter of targetAction.subject.adapters) {
2989
+ for (const adapter of call.guard
2990
+ ? []
2991
+ : targetAction.subject.adapters) {
2966
2992
  if (
2967
2993
  !action.subject.adapters.some(
2968
2994
  (existing) => existing.binding === adapter.binding,
@@ -2979,9 +3005,65 @@ export function compile(
2979
3005
  (cr) =>
2980
3006
  (cr.objectField ?? cr.field.name) === targetObjFieldName,
2981
3007
  );
3008
+ const guardValue = (value: UdlValue): UdlValue => {
3009
+ if (
3010
+ !("field" in value) ||
3011
+ !value.field.startsWith("subject.")
3012
+ )
3013
+ return value;
3014
+ const requirement = action.subject?.requirements.find(
3015
+ (item) => item.field.name === value.field.slice(8),
3016
+ );
3017
+ return {
3018
+ field: `subject.${requirement?.objectField ?? requirement?.field.name ?? value.field.slice(8)}`,
3019
+ };
3020
+ };
3021
+ const guardType =
3022
+ call.guard &&
3023
+ [call.guard.left, call.guard.right]
3024
+ .flatMap((value) =>
3025
+ "field" in value
3026
+ ? [
3027
+ resolveField(
3028
+ document,
3029
+ inst,
3030
+ value.field,
3031
+ action.input,
3032
+ action,
3033
+ )?.type,
3034
+ ]
3035
+ : [],
3036
+ )
3037
+ .find((type) => type === "money" || type === "date");
3038
+ const condition:
3039
+ | NonNullable<UdlSubjectRequirement["when"]>[number][number]
3040
+ | undefined = call.guard
3041
+ ? {
3042
+ instrument: inst.id,
3043
+ action: actionName,
3044
+ guard: {
3045
+ ...call.guard,
3046
+ left: guardValue(call.guard.left),
3047
+ right: guardValue(call.guard.right),
3048
+ },
3049
+ ...(guardType === "money" || guardType === "date"
3050
+ ? { valueType: guardType }
3051
+ : {}),
3052
+ }
3053
+ : undefined;
3054
+ const when = condition
3055
+ ? (targetReq.when ?? [[]]).map((path) => [
3056
+ condition,
3057
+ ...path.filter(
3058
+ (item) =>
3059
+ canonicalJson(item) !== canonicalJson(condition),
3060
+ ),
3061
+ ])
3062
+ : targetReq.when;
2982
3063
  if (!existing) {
2983
- const inherited = {
3064
+ const inherited: UdlSubjectRequirement = {
2984
3065
  field: { ...targetReq.field, name: targetObjFieldName },
3066
+ ...(when ? { when: when.map((path) => [...path]) } : {}),
2985
3067
  };
2986
3068
  action.subject.requirements.push(inherited);
2987
3069
  requirementOrigins.set(
@@ -3002,6 +3084,23 @@ export function compile(
3002
3084
  source: first.source,
3003
3085
  related: [first, second],
3004
3086
  });
3087
+ } else if (existing.when) {
3088
+ if (!when) {
3089
+ delete existing.when;
3090
+ changedInvocations = true;
3091
+ } else {
3092
+ for (const path of when) {
3093
+ if (
3094
+ !existing.when.some(
3095
+ (known) =>
3096
+ canonicalJson(known) === canonicalJson(path),
3097
+ )
3098
+ ) {
3099
+ existing.when.push(path);
3100
+ changedInvocations = true;
3101
+ }
3102
+ }
3103
+ }
3005
3104
  }
3006
3105
  }
3007
3106
  }