@hyperscale0/hsx 4.0.0 → 4.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.
package/src/compile.ts CHANGED
@@ -545,17 +545,6 @@ export function compile(
545
545
  }
546
546
  }
547
547
  const origins: CompileOriginMapEntry[] = [];
548
- const materialApprovals = new Set<UdlAction>();
549
- const implicitDecisions = new Map<
550
- string,
551
- {
552
- target: string;
553
- action: string;
554
- party: string;
555
- protectedRequest: string;
556
- origin: Span;
557
- }
558
- >();
559
548
  const resolveFamily = (
560
549
  rawPath: string,
561
550
  expr: { span: Span; source?: string },
@@ -629,7 +618,6 @@ export function compile(
629
618
  arguments_: BlockExpr,
630
619
  origin: Span,
631
620
  inherited = new Map<string, Expr>(),
632
- inheritedApprovers = new Set<string>(),
633
621
  inheritedEnums = new Map<string, string[]>(),
634
622
  attachmentInfo?: {
635
623
  subjectKindId: string;
@@ -696,17 +684,18 @@ export function compile(
696
684
  return result;
697
685
  };
698
686
 
699
- const resolveChildExportPath = (
687
+ const collectChildExportPaths = (
700
688
  parentDecl: InstrumentDecl,
701
689
  suffix: string,
702
- ): string | undefined => {
690
+ ): string[] => {
703
691
  const recs = entries(asBlock(entries(parentDecl.body).get("records")));
692
+ const matches: string[] = [];
704
693
  for (const [recName] of recs) {
705
- if (suffix === recName) return recName;
694
+ if (suffix === recName) matches.push(recName);
706
695
  }
707
696
  for (const [recName, recBlock] of recs) {
708
697
  if (suffix.startsWith(`${recName}_`)) {
709
- const rest = resolveChildExportPath(
698
+ const nested = collectChildExportPaths(
710
699
  {
711
700
  kind: "instrument",
712
701
  name: recName,
@@ -716,10 +705,28 @@ export function compile(
716
705
  },
717
706
  suffix.slice(recName.length + 1),
718
707
  );
719
- if (rest) return `${recName}.${rest}`;
708
+ for (const rest of nested) {
709
+ matches.push(`${recName}.${rest}`);
710
+ }
720
711
  }
721
712
  }
722
- return undefined;
713
+ return matches;
714
+ };
715
+
716
+ const resolveChildExportPath = (
717
+ parentDecl: InstrumentDecl,
718
+ suffix: string,
719
+ ): string | undefined => {
720
+ const matches = collectChildExportPaths(parentDecl, suffix);
721
+ if (matches.length > 1) {
722
+ failWithCode(
723
+ parentDecl,
724
+ "HSX1001",
725
+ `ambiguous child export path suffix '${suffix}': multiple candidates (${matches.join(", ")})`,
726
+ "rename conflicting records to remove duplicate export path suffixes",
727
+ );
728
+ }
729
+ return matches[0];
723
730
  };
724
731
 
725
732
  const getInstrumentFamily = (targetId: string): UdlFamily | undefined => {
@@ -803,7 +810,6 @@ export function compile(
803
810
  if (type.kind === "call" && type.name === "enum")
804
811
  enums.set(parameter.key, type.args.map(text));
805
812
  }
806
- const approvers = new Set(inheritedApprovers);
807
813
  const supplied = new Map<string, Expr>();
808
814
  for (const entry of arguments_.entries) {
809
815
  if (supplied.has(entry.key))
@@ -822,8 +828,7 @@ export function compile(
822
828
  param.value.kind === "default" ? param.value.value : undefined;
823
829
  const typeName =
824
830
  type.kind === "type" || type.kind === "call" ? type.name : text(type);
825
- const partyParameter =
826
- attachmentInfo && (typeName === "party" || typeName === "approval");
831
+ const partyParameter = attachmentInfo && typeName === "party";
827
832
  const byName: Expr | undefined =
828
833
  partyParameter &&
829
834
  (subjectPartyRoles.includes(param.key as SubjectPartyRole) ||
@@ -981,11 +986,7 @@ export function compile(
981
986
  const v =
982
987
  (supplied.has(param.key) && !attachmentInfo) || type === "enum"
983
988
  ? actual
984
- : resolve(
985
- actual,
986
- new Set(),
987
- !!attachmentInfo && (type === "party" || type === "approval"),
988
- );
989
+ : resolve(actual, new Set(), !!attachmentInfo && type === "party");
989
990
  environment.set(param.key, v);
990
991
  if (type === "enum" && t.kind === "call") {
991
992
  if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
@@ -997,7 +998,7 @@ export function compile(
997
998
  } else if (type === "list") {
998
999
  if (v.kind !== "list")
999
1000
  fail(v, `${param.key} needs a list`, "write [value, value]");
1000
- } else if (type === "party" || type === "approval") {
1001
+ } else if (type === "party") {
1001
1002
  if (v.kind !== "name" || !isParty(v.value))
1002
1003
  failWithCode(
1003
1004
  actual,
@@ -1007,18 +1008,14 @@ export function compile(
1007
1008
  );
1008
1009
  const party = document.parties[v.value];
1009
1010
  if (
1010
- (type === "approval" && (party?.kind !== "staff" || !party.role)) ||
1011
- (type === "party" &&
1012
- (party?.kind === "staff" ||
1013
- (attachmentInfo && party?.kind === "person")))
1011
+ (party?.kind === "staff" && !party.role) ||
1012
+ (attachmentInfo && party?.kind === "person")
1014
1013
  )
1015
1014
  failWithCode(
1016
1015
  actual,
1017
1016
  "party_kind_mismatch",
1018
1017
  `${param.key} cannot bind ${v.value}`,
1019
- type === "approval"
1020
- ? "use a declared staff party with a role"
1021
- : "use a subject role or declared business",
1018
+ "use a subject role, declared business, or staff with a role",
1022
1019
  );
1023
1020
  resolvedParties.add(param.key);
1024
1021
  if (attachmentInfo)
@@ -1027,7 +1024,6 @@ export function compile(
1027
1024
  )
1028
1025
  ? { role: v.value as SubjectPartyRole }
1029
1026
  : { party: v.value };
1030
- if (type === "approval") approvers.add(text(v));
1031
1027
  } else if (type === "ref") {
1032
1028
  const values = v.kind === "list" ? v.items : [v];
1033
1029
  if (v.kind === "list" && (t.kind !== "type" || !t.many))
@@ -1073,6 +1069,14 @@ export function compile(
1073
1069
  fail(value, "duplicate reference", "list each object once");
1074
1070
  seen.add(value.value);
1075
1071
  }
1072
+ // A many-reference tunable is a list even when one object is bound,
1073
+ // so report datasets and other value positions never see a bare name.
1074
+ if (t.kind === "type" && t.many && v.kind !== "list")
1075
+ environment.set(param.key, {
1076
+ kind: "list",
1077
+ items: [v],
1078
+ span: v.span,
1079
+ });
1076
1080
  } else if (type === "fee" || type === "split" || type === "policy") {
1077
1081
  if (v.kind !== "block")
1078
1082
  fail(v, `${param.key} needs a block`, `write ${param.key} { ... }`);
@@ -1438,6 +1442,14 @@ export function compile(
1438
1442
  fail(row, "reference needs a target", "write ref<object>");
1439
1443
  const [root, ...tail] = target.split(".");
1440
1444
  const resolved = environment.get(root!);
1445
+ // An optional field pointing at an unsupplied optional tunable
1446
+ // has nothing to point at, so the instrument drops it.
1447
+ if (
1448
+ !resolved &&
1449
+ f.optional &&
1450
+ decl.parameters.some((parameter) => parameter.key === root)
1451
+ )
1452
+ continue;
1441
1453
  const resolvedTargets =
1442
1454
  resolved?.kind === "list"
1443
1455
  ? resolved.items
@@ -1769,10 +1781,18 @@ export function compile(
1769
1781
  const adapterList: UdlActionSubject["adapters"] = [];
1770
1782
  for (const entry of subjectBlock.entries) {
1771
1783
  if (entry.key === "adapter") {
1784
+ // A tunable names the adapter; anything else is the binding itself.
1785
+ const adapterName = (item: Expr): string =>
1786
+ item.kind === "name" &&
1787
+ decl.parameters.some(
1788
+ (parameter) => parameter.key === item.value,
1789
+ )
1790
+ ? text(resolve(item))
1791
+ : text(item);
1772
1792
  const bindingNames =
1773
1793
  entry.value.kind === "list"
1774
- ? entry.value.items.map(text)
1775
- : [text(entry.value)];
1794
+ ? entry.value.items.map(adapterName)
1795
+ : [adapterName(entry.value)];
1776
1796
  for (const bindingName of bindingNames) {
1777
1797
  const target = options.adapterRegistry?.[bindingName];
1778
1798
  if (target) {
@@ -2265,13 +2285,6 @@ export function compile(
2265
2285
  "choose create, reserve, post, or void from internal_transfer",
2266
2286
  );
2267
2287
  }
2268
- } else if (key === "approval") {
2269
- const approval = data(expr) as UdlAction["approval"];
2270
- if (approval && (approval.input as unknown) === "material") {
2271
- approval.input = {};
2272
- materialApprovals.add(a);
2273
- }
2274
- a.approval = approval;
2275
2288
  } else (a as unknown as Record<string, unknown>)[key] = data(expr);
2276
2289
  }
2277
2290
  if (attachmentInfo) {
@@ -2309,34 +2322,6 @@ export function compile(
2309
2322
  checkSubjectPaths(a.invoke, row.span);
2310
2323
  currentAction = undefined;
2311
2324
  currentActionName = undefined;
2312
- for (const requirement of a.requires ?? []) {
2313
- if (
2314
- requirement.kind !== "approval" ||
2315
- !approvers.has(requirement.party)
2316
- )
2317
- continue;
2318
- const action = requirement.action ?? name;
2319
- const key = `${id}_${action}_decision`;
2320
- const previous = implicitDecisions.get(key);
2321
- if (
2322
- previous &&
2323
- (previous.party !== requirement.party ||
2324
- previous.protectedRequest !==
2325
- (requirement.protectedRequest ?? "self"))
2326
- )
2327
- fail(
2328
- { span: origin },
2329
- `action ${action} has conflicting approval parties or protected requests`,
2330
- "use a separate decision action for each party",
2331
- );
2332
- implicitDecisions.set(key, {
2333
- target: id,
2334
- action,
2335
- party: requirement.party,
2336
- protectedRequest: requirement.protectedRequest ?? "self",
2337
- origin,
2338
- });
2339
- }
2340
2325
  for (const requirement of a.subject?.requirements ?? []) {
2341
2326
  const entry =
2342
2327
  asBlock(subjectExpr).entries.find(
@@ -2401,7 +2386,6 @@ export function compile(
2401
2386
  ...environment,
2402
2387
  ["parent", { kind: "name", value: id, span: origin } as Expr],
2403
2388
  ]),
2404
- approvers,
2405
2389
  enums,
2406
2390
  attachmentInfo
2407
2391
  ? { ...attachmentInfo, exposed: new Map() }
@@ -2506,7 +2490,6 @@ export function compile(
2506
2490
  tunableBlock,
2507
2491
  entry.span,
2508
2492
  new Map(),
2509
- new Set(),
2510
2493
  new Map(),
2511
2494
  {
2512
2495
  subjectKindId: decl.name,
@@ -2643,7 +2626,6 @@ export function compile(
2643
2626
  decl.body,
2644
2627
  decl.span,
2645
2628
  new Map(),
2646
- new Set(),
2647
2629
  new Map(),
2648
2630
  undefined,
2649
2631
  templateFamily ? { ...templateFamily } : undefined,
@@ -2861,145 +2843,6 @@ export function compile(
2861
2843
  );
2862
2844
  }
2863
2845
  }
2864
- // Approval tunables instantiate the same library record as an explicit decision.
2865
- if (implicitDecisions.size) {
2866
- const content = (
2867
- options.standardLibrary ?? bundledStandardLibrary
2868
- ).source("approvals");
2869
- const parsed = content ? parseProgram(content) : undefined;
2870
- const template = parsed?.program.decls.find(
2871
- (decl) => decl.kind === "instrument" && decl.name === "decision",
2872
- );
2873
- if (
2874
- !template ||
2875
- template.kind !== "instrument" ||
2876
- parsed?.diagnostics.length
2877
- )
2878
- fail(
2879
- program,
2880
- "implicit decisions need the approvals header",
2881
- "provide the standard approvals header",
2882
- );
2883
- for (const [id, decision] of implicitDecisions) {
2884
- if (
2885
- !document.instruments.find((inst) => inst.id === decision.target)
2886
- ?.actions[decision.action]
2887
- )
2888
- continue;
2889
- const existing = document.instruments.some((inst) =>
2890
- Object.values(inst.actions).some(
2891
- (action) =>
2892
- action.approval?.action === decision.action &&
2893
- action.approval.party === decision.party &&
2894
- inst.fields.some((field) => {
2895
- if (
2896
- field.type !== "ref" ||
2897
- field.target !== decision.target ||
2898
- `self.${field.name}` !== action.approval?.target
2899
- )
2900
- return false;
2901
- const [root, name, ...tail] =
2902
- decision.protectedRequest.split(".");
2903
- const input = name && action.approval.input[name];
2904
- const request =
2905
- root === "self"
2906
- ? [action.approval.target, name, ...tail]
2907
- .filter(Boolean)
2908
- .join(".")
2909
- : root === "input" && name
2910
- ? input && "field" in input
2911
- ? [input.field, ...tail].join(".")
2912
- : materialApprovals.has(action)
2913
- ? [`self.material_${name}`, ...tail].join(".")
2914
- : undefined
2915
- : undefined;
2916
- return (
2917
- request !== undefined &&
2918
- action.approval.protectedRequest === request
2919
- );
2920
- }),
2921
- ),
2922
- );
2923
- if (existing) continue;
2924
- if (document.instruments.some((inst) => inst.id === id))
2925
- fail(
2926
- { span: decision.origin },
2927
- `implicit decision name ${id} is already used`,
2928
- "rename the conflicting object",
2929
- );
2930
- const protectedRequest =
2931
- decision.protectedRequest === "self"
2932
- ? "self.target"
2933
- : decision.protectedRequest.startsWith("self.")
2934
- ? `self.target.${decision.protectedRequest.slice(5)}`
2935
- : fail(
2936
- { span: decision.origin },
2937
- "implicit approval needs a stored protected request",
2938
- "declare an explicit decision for an input-based protected request",
2939
- );
2940
- addInstrument(
2941
- template,
2942
- id,
2943
- {
2944
- kind: "block",
2945
- span: decision.origin,
2946
- entries: Object.entries({
2947
- for: decision.target,
2948
- approved_by: decision.party,
2949
- action: decision.action,
2950
- protected_request: protectedRequest,
2951
- }).map(([key, value]) => ({
2952
- key,
2953
- value: {
2954
- kind:
2955
- key === "action" || key === "protected_request"
2956
- ? "text"
2957
- : "name",
2958
- value,
2959
- span: decision.origin,
2960
- },
2961
- span: decision.origin,
2962
- })),
2963
- },
2964
- decision.origin,
2965
- new Map(),
2966
- new Set(),
2967
- new Map(),
2968
- document.instruments.find(
2969
- (instrument) => instrument.id === decision.target,
2970
- )?.subject
2971
- ? {
2972
- subjectKindId: document.instruments.find(
2973
- (instrument) => instrument.id === decision.target,
2974
- )!.subject!,
2975
- attachmentName: id,
2976
- parties: {},
2977
- renames: new Map(),
2978
- exposed: new Map(),
2979
- }
2980
- : undefined,
2981
- );
2982
- }
2983
- }
2984
- // Material approval fields are inferred from the selected action's typed input.
2985
- for (const inst of document.instruments)
2986
- for (const action of Object.values(inst.actions)) {
2987
- if (!materialApprovals.has(action) || !action.approval) continue;
2988
- const targetField = inst.fields.find(
2989
- (f) => `self.${f.name}` === action.approval!.target,
2990
- );
2991
- if (targetField?.type !== "ref") continue;
2992
- const target = document.instruments.find(
2993
- (i) => i.id === targetField.target,
2994
- )?.actions[action.approval.action];
2995
- if (!target) continue;
2996
- for (const field of target.input) {
2997
- const name = `material_${field.name}`;
2998
- if (!inst.fields.some((f) => f.name === name))
2999
- inst.fields.push({ ...field, name });
3000
- action.approval.input[field.name] = { field: `self.${name}` };
3001
- }
3002
- }
3003
2846
  const changed = new Set<string>();
3004
2847
  for (const decl of program.decls)
3005
2848
  if (decl.kind === "hide" || decl.kind === "expose") {
package/src/headers.ts CHANGED
@@ -11,7 +11,6 @@ export const HEADER_NAMES = [
11
11
  "financing",
12
12
  "lending",
13
13
  "insurance",
14
- "approvals",
15
14
  "collections",
16
15
  "travel",
17
16
  "cards",
@@ -144,7 +143,7 @@ export function headerManifest(
144
143
  : [];
145
144
  })(),
146
145
  parties: tunables
147
- .filter((t) => ["party", "approval"].includes(t.type))
146
+ .filter((t) => t.type === "party")
148
147
  .map((t) => t.name),
149
148
  actions: actions(decl.body),
150
149
  };
package/src/lex.ts CHANGED
@@ -26,7 +26,6 @@ export const KEYWORDS = [
26
26
  "by",
27
27
  "for",
28
28
  "is",
29
- "approval",
30
29
  "unique",
31
30
  "on",
32
31
  "count",
package/src/parse.ts CHANGED
@@ -330,20 +330,6 @@ class Parser {
330
330
  return this.node(operator.text);
331
331
  }
332
332
  private requirement(): BlockExpr {
333
- if (this.eat("approval")) {
334
- this.expect("by");
335
- const party = this.atom();
336
- const values: Record<string, Expr> = {
337
- kind: this.node("approval"),
338
- party,
339
- decision: this.node("approved"),
340
- };
341
- if (this.eat("for")) values.action = this.atom();
342
- if (this.eat("is")) values.decision = this.atom();
343
- for (const key of ["protectedRequest", "differentFromInitiator"])
344
- if (this.eat(key)) values[key] = this.atom();
345
- return this.record(values);
346
- }
347
333
  if (this.eat("unique")) {
348
334
  const namespace = this.atom();
349
335
  this.expect("on");