@hyperscale0/hsx 5.7.0 → 5.8.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 CHANGED
@@ -1,3 +1,21 @@
1
+ # HSX 5.8.0
2
+
3
+ The checker names misspelled fields, parties and states at the misspelled word,
4
+ with the nearest declared name: "`amont` is not a field or party of `deposit`.
5
+ Did you mean `amount`?" This covers moves, actor parties, and field paths in
6
+ `requires`, `unique ... on`, `set`, `calculate:`, `invoke`, `invariants` and the
7
+ `at:` of `due` and `deadline`. The check runs once on the lowered program, so
8
+ it also names a wrong segment deep in a path ("`balanse` is not a field of
9
+ `self.held`"), `input.x` and `party.x` names, invoke instruments, actions and
10
+ inputs, selection instruments, `where` fields and references, `ref<...>`
11
+ targets and the list of an `at` calculation. Swapped adjacent letters count as
12
+ one edit when picking the suggestion. A state typo in `from`, `to` or `initial` is one error
13
+ instead of an invalid transition plus every unreachable state. A program
14
+ instrument attached before its declaration reports each mistake once.
15
+
16
+ A `ref<T>[]` binding accepts a list of sibling attachments, so `on: [plan]`
17
+ compiles to the same program as `on: plan`.
18
+
1
19
  # HSX 5.7.0
2
20
 
3
21
  Follows UDL 4.9.0. Object `entryActions` admits enrolled customers through named
@@ -1 +1 @@
1
- {"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/compile.ts"],"names":[],"mappings":"AAEA,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AACvE,OAAO,EAeL,KAAK,WAAW,EAOjB,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EAIL,KAAK,UAAU,EAKf,KAAK,IAAI,EACV,MAAM,UAAU,CAAC;AAClB,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,WAAW,iBAAkB,SAAQ,UAAU;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;CACpC;AACD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,aAAa,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3E;AACD,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;CACnB;AACD,MAAM,WAAW,cAAc;IAC7B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,eAAe,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC;CAClE;AACD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,SAAS,CAAC,EAAE;QACV,QAAQ,EAAE,WAAW,CAAC;QACtB,YAAY,EAAE,eAAe,CAAC;QAC9B,SAAS,EAAE,qBAAqB,EAAE,CAAC;KACpC,CAAC;CACH;AAqTD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,cAAmB,GAC3B,aAAa,CA8yGf"}
1
+ {"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../src/compile.ts"],"names":[],"mappings":"AAOA,OAAO,EAAwB,KAAK,eAAe,EAAE,MAAM,WAAW,CAAC;AACvE,OAAO,EAeL,KAAK,WAAW,EAQjB,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EAIL,KAAK,UAAU,EAKf,KAAK,IAAI,EACV,MAAM,UAAU,CAAC;AAClB,OAAO,EAA0B,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEhF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,WAAW,iBAAkB,SAAQ,UAAU;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;CACpC;AACD,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,aAAa,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3E;AACD,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,eAAe,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;CACnB;AACD,MAAM,WAAW,cAAc;IAC7B,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,eAAe,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC,CAAC;CAClE;AACD,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,OAAO,GAAG,SAAS,CAAC;IAC7B,WAAW,EAAE,iBAAiB,EAAE,CAAC;IACjC,SAAS,CAAC,EAAE;QACV,QAAQ,EAAE,WAAW,CAAC;QACtB,YAAY,EAAE,eAAe,CAAC;QAC9B,SAAS,EAAE,qBAAqB,EAAE,CAAC;KACpC,CAAC;CACH;AAqTD,wBAAgB,OAAO,CACrB,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,cAAmB,GAC3B,aAAa,CAwnHf"}
@@ -1,4 +1,4 @@
1
- import { CompileFailure, fail, failWithCode } from "./diagnostics.js";
1
+ import { CompileFailure, didYouMean, fail, failWithCode, } from "./diagnostics.js";
2
2
  import { hash as sha256 } from "fast-sha256";
3
3
  import { buildUdlCostManifest } from "./cost.js";
4
4
  import { validateUdl, resolveField, sameObjectField, subjectPartyRoles, RESERVED_OBJECT_NAMES, udlObjectFieldSchema, udlInstrumentSchema, } from "@hyperscale0/udl";
@@ -253,6 +253,11 @@ export function compile(source, options = {}) {
253
253
  severity: "error",
254
254
  stage,
255
255
  });
256
+ // A program instrument lowers once standalone and once per attachment, so
257
+ // one authored mistake can fail each copy at the same place.
258
+ const distinct = (list) => list.filter((d, index) => list.findIndex((other) => other.source === d.source &&
259
+ other.span.start === d.span.start &&
260
+ other.message === d.message) === index);
256
261
  if (parsed.diagnostics.length)
257
262
  return {
258
263
  verdict: "invalid",
@@ -403,6 +408,10 @@ export function compile(source, options = {}) {
403
408
  }
404
409
  }
405
410
  const origins = [];
411
+ // Lowered objects lose their spans, so each keeps the expression that
412
+ // produced it for the name check after lowering.
413
+ const authored = new WeakMap();
414
+ const declarations = new WeakMap();
406
415
  const exposures = [];
407
416
  const refundBindings = new Map();
408
417
  const resolveFamily = (rawPath, expr, required = true) => {
@@ -807,7 +816,8 @@ export function compile(source, options = {}) {
807
816
  continue;
808
817
  const t = param.value.kind === "default" ? param.value.type : param.value;
809
818
  const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
810
- let v = (supplied.has(param.key) && !attachmentInfo) || type === "enum"
819
+ const asWritten = (supplied.has(param.key) && !attachmentInfo) || type === "enum";
820
+ let v = asWritten
811
821
  ? actual
812
822
  : resolve(actual, new Set(), !!attachmentInfo && type === "party");
813
823
  if (type === "duration" &&
@@ -843,7 +853,15 @@ export function compile(source, options = {}) {
843
853
  : { party: v.value };
844
854
  }
845
855
  else if (type === "ref") {
846
- const values = v.kind === "list" ? v.items : [v];
856
+ // Listed objects resolve like a single binding, so `on: [plan]`
857
+ // names the sibling attachment that `on: plan` names.
858
+ const values = v.kind !== "list"
859
+ ? [v]
860
+ : asWritten
861
+ ? v.items
862
+ : v.items.map((item) => resolve(item));
863
+ if (v.kind === "list")
864
+ environment.set(param.key, { ...v, items: values });
847
865
  if (v.kind === "list" && (t.kind !== "type" || !t.many))
848
866
  fail(v, `${param.key} accepts one reference`, "use one object name");
849
867
  if (!values.length || values.length > 16)
@@ -1093,9 +1111,11 @@ export function compile(source, options = {}) {
1093
1111
  };
1094
1112
  const val = (expr) => {
1095
1113
  const v = resolve(expr);
1096
- return v.kind === "name" && !["true", "false"].includes(v.value)
1097
- ? { field: path(v) }
1098
- : { literal: literal(v) };
1114
+ if (v.kind !== "name" || ["true", "false"].includes(v.value))
1115
+ return { literal: literal(v) };
1116
+ const value = { field: path(v) };
1117
+ authored.set(value, expr);
1118
+ return value;
1099
1119
  };
1100
1120
  const data = (expr) => {
1101
1121
  if (expr.kind === "call" &&
@@ -1139,6 +1159,7 @@ export function compile(source, options = {}) {
1139
1159
  continue;
1140
1160
  result[k] = data(v);
1141
1161
  }
1162
+ authored.set(result, value);
1142
1163
  return result;
1143
1164
  }
1144
1165
  const result = Object.fromEntries([...rawEntries].map(([key, value]) => [key, data(value)]));
@@ -1160,10 +1181,14 @@ export function compile(source, options = {}) {
1160
1181
  };
1161
1182
  return null;
1162
1183
  }
1184
+ authored.set(result, value);
1163
1185
  return result;
1164
1186
  }
1165
- if (value.kind === "list")
1166
- return value.items.map(data).filter((item) => item !== null);
1187
+ if (value.kind === "list") {
1188
+ const items = value.items.map(data).filter((item) => item !== null);
1189
+ authored.set(items, value);
1190
+ return items;
1191
+ }
1167
1192
  if (value.kind === "call" &&
1168
1193
  value.name === "child" &&
1169
1194
  value.args.length === 2) {
@@ -1370,6 +1395,7 @@ export function compile(source, options = {}) {
1370
1395
  });
1371
1396
  else
1372
1397
  fail(constant, `unknown calculation ${constant.name}`, "use rate, sum, minimum, subtract, multiply, divide, before or after");
1398
+ authored.set(calculations.at(-1), constant);
1373
1399
  }
1374
1400
  else if (type === "enum" && constant.kind === "name")
1375
1401
  f.value = constant.value;
@@ -1382,12 +1408,25 @@ export function compile(source, options = {}) {
1382
1408
  else
1383
1409
  f.value = literal(constant, String(type));
1384
1410
  }
1411
+ authored.set(f, row.value);
1385
1412
  result.push(f);
1386
1413
  }
1387
1414
  return result;
1388
1415
  };
1389
1416
  fields.push(...lowerFields(asBlock(body.get("fields"))));
1390
- const lifecycle = data(asBlock(body.get("lifecycle")));
1417
+ const lifecycleBlock = asBlock(body.get("lifecycle"));
1418
+ const lifecycle = data(lifecycleBlock);
1419
+ const states = Array.isArray(lifecycle.states) ? lifecycle.states : [];
1420
+ const checkState = (expr, state) => {
1421
+ if (typeof state !== "string" || !states.length)
1422
+ return;
1423
+ if (states.includes(state))
1424
+ return;
1425
+ fail(expr, `\`${state}\` is not a state of \`${decl.name}\`.${didYouMean(state, states)}`, `Use one of ${states.join(", ")}, or add \`${state}\` to \`lifecycle.states\`.`);
1426
+ };
1427
+ const initialExpr = entries(lifecycleBlock).get("initial");
1428
+ if (initialExpr)
1429
+ checkState(initialExpr, lifecycle.initial);
1391
1430
  lifecycle.transitions = Object.create(null);
1392
1431
  const inst = {
1393
1432
  id,
@@ -1656,6 +1695,7 @@ export function compile(source, options = {}) {
1656
1695
  moves: [],
1657
1696
  ...(actionSubject ? { subject: actionSubject } : {}),
1658
1697
  };
1698
+ authored.set(a, row.value);
1659
1699
  for (const binding of boundaryBindings)
1660
1700
  if (!a.subject?.adapters.find((entry) => entry.binding === binding)
1661
1701
  ?.snapshot)
@@ -1667,10 +1707,13 @@ export function compile(source, options = {}) {
1667
1707
  const to = slots.get("to");
1668
1708
  if (!from || !to)
1669
1709
  fail(row, `action ${name} needs from and to`, "declare its source and destination states");
1670
- lifecycle.transitions[name] = {
1671
- from: (from.kind === "list" ? from.items : [from]).map((e) => String(data(e))),
1672
- to: String(data(to)),
1673
- };
1710
+ const sources = from.kind === "list" ? from.items : [from];
1711
+ const fromStates = sources.map((e) => String(data(e)));
1712
+ const toState = String(data(to));
1713
+ sources.forEach((e, index) => checkState(e, fromStates[index]));
1714
+ if (toState !== "preserve")
1715
+ checkState(to, toState);
1716
+ lifecycle.transitions[name] = { from: fromStates, to: toState };
1674
1717
  }
1675
1718
  for (const [key, expr] of slots) {
1676
1719
  if (["from", "to", "input", "summary", "subject"].includes(key))
@@ -1678,6 +1721,7 @@ export function compile(source, options = {}) {
1678
1721
  if (key === "moves") {
1679
1722
  const moves = expr.kind === "list" ? expr.items : [expr];
1680
1723
  for (const [index, move] of moves.entries()) {
1724
+ const lowered = a.moves.length;
1681
1725
  const parts = entries(asBlock(move));
1682
1726
  const op = parts.has("operation")
1683
1727
  ? String(data(parts.get("operation")))
@@ -1831,6 +1875,8 @@ export function compile(source, options = {}) {
1831
1875
  }
1832
1876
  if (total !== 10000)
1833
1877
  fail(move, "split percentages must sum to 100%", "give the recipients exactly 100% together");
1878
+ for (const item of a.moves.slice(lowered))
1879
+ authored.set(item, move);
1834
1880
  continue;
1835
1881
  }
1836
1882
  if (!amount || !from || !to)
@@ -1972,6 +2018,8 @@ export function compile(source, options = {}) {
1972
2018
  });
1973
2019
  else
1974
2020
  fail(move, `unknown transfer instruction ${op}`, "choose create, reserve, post, or void from internal_transfer");
2021
+ for (const item of a.moves.slice(lowered))
2022
+ authored.set(item, move);
1975
2023
  }
1976
2024
  }
1977
2025
  else
@@ -2090,6 +2138,7 @@ export function compile(source, options = {}) {
2090
2138
  };
2091
2139
  }
2092
2140
  document.instruments.push(inst);
2141
+ declarations.set(inst, decl);
2093
2142
  for (const [key, definition] of records) {
2094
2143
  const childAttachment = attachmentInfo
2095
2144
  ? {
@@ -2337,7 +2386,7 @@ export function compile(source, options = {}) {
2337
2386
  if (bindingDiagnostics.length)
2338
2387
  return {
2339
2388
  verdict: "invalid",
2340
- diagnostics: bindingDiagnostics
2389
+ diagnostics: distinct(bindingDiagnostics)
2341
2390
  .sort((a, b) => a.span.start - b.span.start)
2342
2391
  .map((d) => diagnostic(d, "check")),
2343
2392
  };
@@ -2521,6 +2570,220 @@ export function compile(source, options = {}) {
2521
2570
  failWithCode(objects.get(kind.id), "subject_field_unknown", `column ${column} is unknown on ${kind.id}`, "name an authored field or attached requirement");
2522
2571
  }
2523
2572
  }
2573
+ const byId = new Map(document.instruments.map((inst) => [inst.id, inst]));
2574
+ const site = (owner, key, fallback) => {
2575
+ const expr = authored.get(owner);
2576
+ if (expr?.kind === "block")
2577
+ return expr.entries.find((entry) => entry.key === key)?.value ?? expr;
2578
+ if (expr?.kind === "list" && Array.isArray(owner))
2579
+ return expr.items.length === owner.length
2580
+ ? expr.items[Number(key)]
2581
+ : expr;
2582
+ return expr ?? fallback;
2583
+ };
2584
+ const keySite = (owner, key, fallback) => {
2585
+ const expr = authored.get(owner);
2586
+ const entry = expr?.kind === "block"
2587
+ ? expr.entries.find((item) => item.key === key)
2588
+ : undefined;
2589
+ if (!entry)
2590
+ return fallback;
2591
+ return {
2592
+ ...entry,
2593
+ span: { start: entry.span.start, end: entry.span.start + key.length },
2594
+ };
2595
+ };
2596
+ const builtins = (inst) => [
2597
+ "id",
2598
+ "now",
2599
+ "createdAt",
2600
+ "productRevision",
2601
+ "status",
2602
+ ...(inst.subject ? ["subject"] : []),
2603
+ ];
2604
+ const parties = (inst) => [
2605
+ ...Object.keys(document.parties),
2606
+ ...(inst.subject ? subjectPartyRoles : []),
2607
+ ];
2608
+ const members = (field) => {
2609
+ if (field.type === "account")
2610
+ return ["balance", "reserved"];
2611
+ if (field.type !== "ref")
2612
+ return [];
2613
+ const [id] = [field.target].flat();
2614
+ const target = field.targetKind === "object"
2615
+ ? document.objects.find((object) => object.id === id)
2616
+ : byId.get(id);
2617
+ if (!target)
2618
+ return [];
2619
+ return [
2620
+ ...target.fields.map((item) => item.name),
2621
+ ...("lifecycle" in target ? builtins(target) : []),
2622
+ ];
2623
+ };
2624
+ const checkParty = (name, at, inst) => {
2625
+ const names = parties(inst);
2626
+ if (!names.includes(name))
2627
+ fail(at, `\`${name}\` is not a declared party.${didYouMean(name, names)}`, `Declare it with \`party ${name}: business\`, or name a declared party (${names.join(", ")}).`);
2628
+ };
2629
+ const checkInput = (name, at, label, input) => {
2630
+ const names = input.map((field) => field.name);
2631
+ if (!names.includes(name))
2632
+ fail(at, `\`${name}\` is not an input of \`${label}\`.${didYouMean(name, names)}`, `Declare \`${name}\` in the input of \`${label}\`${names.length ? `, or use one of ${names.join(", ")}` : ""}.`);
2633
+ };
2634
+ const checkTarget = (id, at, objects = false) => {
2635
+ const ids = [
2636
+ ...byId.keys(),
2637
+ ...(objects ? document.objects.map((object) => object.id) : []),
2638
+ ];
2639
+ const noun = objects ? "instrument or object" : "instrument";
2640
+ if (!ids.includes(id))
2641
+ fail(at, `\`${id}\` is not a declared ${noun}.${didYouMean(id, ids)}`, `Name a declared ${noun}.`);
2642
+ };
2643
+ const checkPath = (path, at, scope) => {
2644
+ const { inst, action } = scope;
2645
+ const parts = path.split(".");
2646
+ const found = (count) => resolveField(document, inst, parts.slice(0, count).join("."), action?.input, action);
2647
+ // Subject paths keep their own check against the action's requirements.
2648
+ if (found(parts.length) || parts[0] === "subject")
2649
+ return;
2650
+ if (parts[0] === "party" && parts[1]) {
2651
+ checkParty(parts[1], at, inst);
2652
+ if (!found(2))
2653
+ return;
2654
+ }
2655
+ if (!["self", "input", "party"].includes(parts[0]))
2656
+ parts.unshift("self");
2657
+ let count = 2;
2658
+ while (count <= parts.length && found(count))
2659
+ count++;
2660
+ if (count > parts.length)
2661
+ return;
2662
+ const name = parts[count - 1];
2663
+ if (count > 2) {
2664
+ const prefix = parts.slice(0, count - 1).join(".");
2665
+ const names = members(found(count - 1));
2666
+ fail(at, `\`${name}\` is not a field of \`${prefix}\`.${didYouMean(name, names)}`, names.length
2667
+ ? `Use one of ${names.join(", ")}.`
2668
+ : `\`${prefix}\` has no fields.`);
2669
+ }
2670
+ if (parts[0] === "input")
2671
+ return checkInput(name, at, scope.label, action?.input ?? []);
2672
+ const declared = declarations.get(inst).name;
2673
+ const fields = inst.fields.map((field) => field.name);
2674
+ fail(at, `\`${name}\` is not a field or party of \`${declared}\`.${didYouMean(name, [...fields, ...builtins(inst), ...parties(inst)])}`, `Use a field declared in \`${declared}\` (${fields.join(", ")}) or a declared party (${parties(inst).join(", ")}).`);
2675
+ };
2676
+ const scopeOf = (inst) => ({
2677
+ inst,
2678
+ label: declarations.get(inst).name,
2679
+ });
2680
+ const checkSelection = (selection, at, scope) => {
2681
+ walk(selection.anchor, "anchor", site(selection, "anchor", at), scope);
2682
+ walk(selection.where, "where", site(selection, "where", at), scope);
2683
+ for (const id of [selection.instrument].flat()) {
2684
+ checkTarget(id, site(selection, "instrument", at));
2685
+ const target = scopeOf(byId.get(id));
2686
+ const member = (key, where) => checkPath(key.startsWith("self.") ? key : `self.${key}`, where, target);
2687
+ for (const key of Object.keys(selection.where ?? {}))
2688
+ member(key, keySite(selection.where, key, at));
2689
+ member(selection.reference, site(selection, "reference", at));
2690
+ selection.order?.forEach((key, index) => member(key, site(selection.order, index, at)));
2691
+ if (selection.window)
2692
+ member(selection.window.field, site(selection.window, "field", at));
2693
+ }
2694
+ };
2695
+ const checkCall = (call, at, scope) => {
2696
+ const bind = "range" in call ? call.range?.bind : undefined;
2697
+ for (const [key, value] of Object.entries(call))
2698
+ if (key !== "input")
2699
+ walk(value, key, site(call, key, at), scope);
2700
+ for (const [key, value] of Object.entries(call.input))
2701
+ if (!("field" in value) || value.field !== bind)
2702
+ walk(value, key, site(call.input, key, at), scope);
2703
+ if ("instrument" in call)
2704
+ checkTarget(call.instrument, site(call, "instrument", at));
2705
+ const reference = "reference" in call
2706
+ ? resolveField(document, scope.inst, call.reference, scope.action?.input, scope.action)
2707
+ : undefined;
2708
+ const ids = "instrument" in call
2709
+ ? [call.instrument]
2710
+ : "selection" in call
2711
+ ? [call.selection.instrument].flat()
2712
+ : reference?.type === "ref" && reference.targetKind === "instrument"
2713
+ ? [reference.target].flat()
2714
+ : [];
2715
+ for (const id of ids) {
2716
+ const target = byId.get(id);
2717
+ if (!target)
2718
+ continue;
2719
+ const action = target.actions[call.action];
2720
+ if (!action)
2721
+ fail(site(call, "action", at), `\`${call.action}\` is not an action of \`${id}\`.${didYouMean(call.action, target.actionOrder)}`, `Use one of ${target.actionOrder.join(", ")}.`);
2722
+ for (const key of Object.keys(call.input))
2723
+ checkInput(key, keySite(call.input, key, at), `${id}.${call.action}`, action.input);
2724
+ }
2725
+ };
2726
+ const pathKeys = [
2727
+ "field",
2728
+ "reference",
2729
+ "anchor",
2730
+ "at",
2731
+ "list",
2732
+ "from",
2733
+ "to",
2734
+ "transfer",
2735
+ "subject",
2736
+ "instruction",
2737
+ "installment",
2738
+ "recipient",
2739
+ "dueAt",
2740
+ "fields",
2741
+ ];
2742
+ const walk = (value, key, at, scope) => {
2743
+ if (typeof value === "string") {
2744
+ if (key === "party")
2745
+ checkParty(value, at, scope.inst);
2746
+ if (key === "target")
2747
+ checkPath(`self.${value}`, at, scope);
2748
+ if (pathKeys.includes(key))
2749
+ checkPath(value, at, scope);
2750
+ return;
2751
+ }
2752
+ if (!value || typeof value !== "object")
2753
+ return;
2754
+ if (Array.isArray(value))
2755
+ return value.forEach((item, index) => walk(item, key, site(value, index, at), scope));
2756
+ if (key === "selection")
2757
+ return checkSelection(value, at, scope);
2758
+ if (key === "invoke")
2759
+ return checkCall(value, at, scope);
2760
+ if (key === "set")
2761
+ for (const name of Object.keys(value))
2762
+ checkPath(`self.${name}`, keySite(value, name, at), scope);
2763
+ for (const [child, item] of Object.entries(value))
2764
+ walk(item, child, site(value, child, at), scope);
2765
+ };
2766
+ for (const inst of document.instruments) {
2767
+ const decl = declarations.get(inst);
2768
+ const scope = scopeOf(inst);
2769
+ for (const field of [
2770
+ ...inst.fields,
2771
+ ...Object.values(inst.actions).flatMap((action) => action.input),
2772
+ ])
2773
+ if ("target" in field && field.target !== undefined)
2774
+ for (const id of [field.target].flat())
2775
+ checkTarget(id, site(field, "target", decl), true);
2776
+ walk(inst.calculate, "calculate", decl, scope);
2777
+ walk(inst.invariants, "invariants", decl, scope);
2778
+ for (const [name, action] of Object.entries(inst.actions))
2779
+ for (const [key, value] of Object.entries(action))
2780
+ if (key !== "input" && key !== "subject")
2781
+ walk(value, key, site(action, key, decl), {
2782
+ inst,
2783
+ label: `${decl.name}.${name}`,
2784
+ action,
2785
+ });
2786
+ }
2524
2787
  const eliminatedStates = new Map();
2525
2788
  // Remove branches excluded by immutable tunables before checking reference states.
2526
2789
  for (const inst of document.instruments) {
@@ -2702,7 +2965,7 @@ export function compile(source, options = {}) {
2702
2965
  if (error instanceof CompileFailure)
2703
2966
  return {
2704
2967
  verdict: "invalid",
2705
- diagnostics: [...bindingDiagnostics, error.diagnostic].map((d) => diagnostic(d, "check")),
2968
+ diagnostics: distinct([...bindingDiagnostics, error.diagnostic]).map((d) => diagnostic(d, "check")),
2706
2969
  };
2707
2970
  throw error;
2708
2971
  }