@hyperscale0/hsx 5.6.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,36 @@
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
+
19
+ # HSX 5.7.0
20
+
21
+ Follows UDL 4.9.0. Object `entryActions` admits enrolled customers through named
22
+ actor-bound create actions. Product-scoped limits resolve across records with
23
+ matching parties. `programTax` moves use an operator-owned pass-through account.
24
+
25
+ Late charge creation accepts a slice reference for plans with multiple instalments.
26
+ Financing, collections and lending samples expose repayment child actions.
27
+ Aggregate selectors retain their selected instrument family, including child
28
+ records, so shared limits compare the matching contracts across Builds.
29
+
30
+ # HSX 5.6.1
31
+
32
+ Action summaries without authored text use the action title without the instrument id.
33
+
1
34
  # HSX 5.6.0
2
35
 
3
36
  The compiler records the standard block origin of every instrument compiled
@@ -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,CAktGf"}
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)
@@ -1014,6 +1032,7 @@ export function compile(source, options = {}) {
1014
1032
  "parameterDiagnostics",
1015
1033
  "reports",
1016
1034
  "revisioned",
1035
+ "scope",
1017
1036
  ].includes(key) &&
1018
1037
  !key.startsWith("action "))
1019
1038
  fail(decl, `unknown instrument clause ${key}`, "use fields, lifecycle, actions, invariants, or records");
@@ -1092,9 +1111,11 @@ export function compile(source, options = {}) {
1092
1111
  };
1093
1112
  const val = (expr) => {
1094
1113
  const v = resolve(expr);
1095
- return v.kind === "name" && !["true", "false"].includes(v.value)
1096
- ? { field: path(v) }
1097
- : { 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;
1098
1119
  };
1099
1120
  const data = (expr) => {
1100
1121
  if (expr.kind === "call" &&
@@ -1138,6 +1159,7 @@ export function compile(source, options = {}) {
1138
1159
  continue;
1139
1160
  result[k] = data(v);
1140
1161
  }
1162
+ authored.set(result, value);
1141
1163
  return result;
1142
1164
  }
1143
1165
  const result = Object.fromEntries([...rawEntries].map(([key, value]) => [key, data(value)]));
@@ -1159,10 +1181,14 @@ export function compile(source, options = {}) {
1159
1181
  };
1160
1182
  return null;
1161
1183
  }
1184
+ authored.set(result, value);
1162
1185
  return result;
1163
1186
  }
1164
- if (value.kind === "list")
1165
- 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
+ }
1166
1192
  if (value.kind === "call" &&
1167
1193
  value.name === "child" &&
1168
1194
  value.args.length === 2) {
@@ -1369,6 +1395,7 @@ export function compile(source, options = {}) {
1369
1395
  });
1370
1396
  else
1371
1397
  fail(constant, `unknown calculation ${constant.name}`, "use rate, sum, minimum, subtract, multiply, divide, before or after");
1398
+ authored.set(calculations.at(-1), constant);
1372
1399
  }
1373
1400
  else if (type === "enum" && constant.kind === "name")
1374
1401
  f.value = constant.value;
@@ -1381,12 +1408,25 @@ export function compile(source, options = {}) {
1381
1408
  else
1382
1409
  f.value = literal(constant, String(type));
1383
1410
  }
1411
+ authored.set(f, row.value);
1384
1412
  result.push(f);
1385
1413
  }
1386
1414
  return result;
1387
1415
  };
1388
1416
  fields.push(...lowerFields(asBlock(body.get("fields"))));
1389
- 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);
1390
1430
  lifecycle.transitions = Object.create(null);
1391
1431
  const inst = {
1392
1432
  id,
@@ -1646,7 +1686,7 @@ export function compile(source, options = {}) {
1646
1686
  const a = {
1647
1687
  summary: slots.has("summary")
1648
1688
  ? String(data(slots.get("summary")))
1649
- : `${title(name)} ${id}`,
1689
+ : title(name),
1650
1690
  publicAction: camel(`${name}_${id}`),
1651
1691
  event: `${id}.${name}`,
1652
1692
  actor: "caller",
@@ -1655,6 +1695,7 @@ export function compile(source, options = {}) {
1655
1695
  moves: [],
1656
1696
  ...(actionSubject ? { subject: actionSubject } : {}),
1657
1697
  };
1698
+ authored.set(a, row.value);
1658
1699
  for (const binding of boundaryBindings)
1659
1700
  if (!a.subject?.adapters.find((entry) => entry.binding === binding)
1660
1701
  ?.snapshot)
@@ -1666,10 +1707,13 @@ export function compile(source, options = {}) {
1666
1707
  const to = slots.get("to");
1667
1708
  if (!from || !to)
1668
1709
  fail(row, `action ${name} needs from and to`, "declare its source and destination states");
1669
- lifecycle.transitions[name] = {
1670
- from: (from.kind === "list" ? from.items : [from]).map((e) => String(data(e))),
1671
- to: String(data(to)),
1672
- };
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 };
1673
1717
  }
1674
1718
  for (const [key, expr] of slots) {
1675
1719
  if (["from", "to", "input", "summary", "subject"].includes(key))
@@ -1677,6 +1721,7 @@ export function compile(source, options = {}) {
1677
1721
  if (key === "moves") {
1678
1722
  const moves = expr.kind === "list" ? expr.items : [expr];
1679
1723
  for (const [index, move] of moves.entries()) {
1724
+ const lowered = a.moves.length;
1680
1725
  const parts = entries(asBlock(move));
1681
1726
  const op = parts.has("operation")
1682
1727
  ? String(data(parts.get("operation")))
@@ -1830,6 +1875,8 @@ export function compile(source, options = {}) {
1830
1875
  }
1831
1876
  if (total !== 10000)
1832
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);
1833
1880
  continue;
1834
1881
  }
1835
1882
  if (!amount || !from || !to)
@@ -1971,6 +2018,8 @@ export function compile(source, options = {}) {
1971
2018
  });
1972
2019
  else
1973
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);
1974
2023
  }
1975
2024
  }
1976
2025
  else
@@ -2033,9 +2082,38 @@ export function compile(source, options = {}) {
2033
2082
  inst.actions[name] = a;
2034
2083
  inst.actionOrder.push(name);
2035
2084
  }
2036
- for (const key of ["invariants", "reports", "revisioned"])
2085
+ for (const key of [
2086
+ "invariants",
2087
+ "reports",
2088
+ "revisioned",
2089
+ "scope",
2090
+ ])
2037
2091
  if (body.has(key))
2038
2092
  inst[key] = data(body.get(key));
2093
+ let usesTax = false;
2094
+ for (const action of Object.values(inst.actions)) {
2095
+ for (const move of action.moves) {
2096
+ if (!("from" in move))
2097
+ continue;
2098
+ for (const endpoint of ["from", "to"]) {
2099
+ if (move[endpoint] === "party.programTax") {
2100
+ move[endpoint] = "self.programTaxPayable";
2101
+ usesTax = true;
2102
+ }
2103
+ }
2104
+ }
2105
+ }
2106
+ if (usesTax) {
2107
+ if (inst.fields.some((field) => field.name === "programTaxPayable"))
2108
+ fail(decl, "programTaxPayable is reserved for the tax account", "rename the authored field");
2109
+ inst.fields.push({
2110
+ name: "programTaxPayable",
2111
+ type: "account",
2112
+ owner: "programOperator",
2113
+ book: "cash",
2114
+ key: "programTax",
2115
+ });
2116
+ }
2039
2117
  const shape = udlInstrumentSchema.safeParse(inst);
2040
2118
  if (!shape.success) {
2041
2119
  const issue = shape.error.issues[0];
@@ -2060,6 +2138,7 @@ export function compile(source, options = {}) {
2060
2138
  };
2061
2139
  }
2062
2140
  document.instruments.push(inst);
2141
+ declarations.set(inst, decl);
2063
2142
  for (const [key, definition] of records) {
2064
2143
  const childAttachment = attachmentInfo
2065
2144
  ? {
@@ -2111,8 +2190,9 @@ export function compile(source, options = {}) {
2111
2190
  for (const key of body.keys()) {
2112
2191
  if (key !== "fields" &&
2113
2192
  key !== "columns" &&
2193
+ key !== "entryActions" &&
2114
2194
  !key.startsWith("attach ")) {
2115
- fail(decl, `unknown object clause ${key}`, "use fields, columns, or attach");
2195
+ fail(decl, `unknown object clause ${key}`, "use fields, columns, entryActions, or attach");
2116
2196
  }
2117
2197
  }
2118
2198
  const authoredFields = [];
@@ -2271,9 +2351,17 @@ export function compile(source, options = {}) {
2271
2351
  fail(columnsExpr, `The object list selects ${columns.length} columns; this release supports 8. The object may retain all its fields.`, `Remove ${columns.length === 9 ? "one name" : `${columns.length - 8} names`} from \`columns\`, not from \`fields\`.`);
2272
2352
  }
2273
2353
  }
2354
+ const entryExpr = body.get("entryActions");
2355
+ let entryActions;
2356
+ if (entryExpr) {
2357
+ if (entryExpr.kind !== "list" || !entryExpr.items.length)
2358
+ fail(entryExpr, "entryActions needs a nonempty list", "name exposed create actions bound to actor");
2359
+ entryActions = entryExpr.items.map(text);
2360
+ }
2274
2361
  document.objects.push({
2275
2362
  id: decl.name,
2276
2363
  title: decl.title,
2364
+ ...(entryActions ? { entryActions } : {}),
2277
2365
  authoredFields: authoredNames,
2278
2366
  attachments,
2279
2367
  fields: authoredFields,
@@ -2298,10 +2386,29 @@ export function compile(source, options = {}) {
2298
2386
  if (bindingDiagnostics.length)
2299
2387
  return {
2300
2388
  verdict: "invalid",
2301
- diagnostics: bindingDiagnostics
2389
+ diagnostics: distinct(bindingDiagnostics)
2302
2390
  .sort((a, b) => a.span.start - b.span.start)
2303
2391
  .map((d) => diagnostic(d, "check")),
2304
2392
  };
2393
+ // Resolve selectors after every attachment and child has its family.
2394
+ const retainSelectionFamilies = (value) => {
2395
+ if (!value || typeof value !== "object")
2396
+ return;
2397
+ const record = value;
2398
+ const selection = record.selection;
2399
+ if (selection && !selection.family) {
2400
+ const families = [selection.instrument]
2401
+ .flat()
2402
+ .map((id) => document.instruments.find((item) => item.id === id)?.family);
2403
+ const family = families[0];
2404
+ if (family &&
2405
+ families.every((item) => canonicalJson(item) === canonicalJson(family)))
2406
+ selection.family = family;
2407
+ }
2408
+ for (const child of Object.values(record))
2409
+ retainSelectionFamilies(child);
2410
+ };
2411
+ retainSelectionFamilies(document);
2305
2412
  // Propagate invoked requirements with the conditions on each invocation path.
2306
2413
  let changedInvocations = true;
2307
2414
  let invocationIterations = 0;
@@ -2463,6 +2570,220 @@ export function compile(source, options = {}) {
2463
2570
  failWithCode(objects.get(kind.id), "subject_field_unknown", `column ${column} is unknown on ${kind.id}`, "name an authored field or attached requirement");
2464
2571
  }
2465
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
+ }
2466
2787
  const eliminatedStates = new Map();
2467
2788
  // Remove branches excluded by immutable tunables before checking reference states.
2468
2789
  for (const inst of document.instruments) {
@@ -2557,6 +2878,19 @@ export function compile(source, options = {}) {
2557
2878
  if (Object.values(instrument.actions).filter((action) => action.publicAction === decl.name).length > 1)
2558
2879
  fail(decl, `public action ${decl.name} is used more than once on ${parts.join(".")}`, "give each exposed action a distinct public name");
2559
2880
  }
2881
+ for (const kind of document.objects) {
2882
+ for (const name of kind.entryActions ?? []) {
2883
+ const eligible = kind.attachments.some((attachment) => {
2884
+ const create = document.instruments.find((instrument) => instrument.id === attachment.instrument)?.actions.create;
2885
+ return (create?.publicAction === name &&
2886
+ typeof create.actor === "object" &&
2887
+ "party" in create.actor &&
2888
+ create.actor.party === "actor");
2889
+ });
2890
+ if (!eligible)
2891
+ fail(objects.get(kind.id), `entry action ${name} must create an attachment as actor`, "expose its create action and bind its acting party to actor");
2892
+ }
2893
+ }
2560
2894
  const usedParties = new Set();
2561
2895
  const collectParties = (value) => {
2562
2896
  if (typeof value === "string") {
@@ -2631,7 +2965,7 @@ export function compile(source, options = {}) {
2631
2965
  if (error instanceof CompileFailure)
2632
2966
  return {
2633
2967
  verdict: "invalid",
2634
- diagnostics: [...bindingDiagnostics, error.diagnostic].map((d) => diagnostic(d, "check")),
2968
+ diagnostics: distinct([...bindingDiagnostics, error.diagnostic]).map((d) => diagnostic(d, "check")),
2635
2969
  };
2636
2970
  throw error;
2637
2971
  }