@hyperscale0/hsx 5.7.0 → 6.0.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,35 @@
1
+ # HSX 6.0.0
2
+
3
+ Breaking: the `travel` std header is gone, with its `travel.package` and
4
+ `travel.booking` instruments and the travel agency sample. Programs that
5
+ `use travel` no longer compile; build a booking from `escrow.hold` and an
6
+ authored instrument instead. `reporting.portfolio` drops the
7
+ `origination_cohorts` and `bureau_facility` report definitions, which refused
8
+ every request. The compiler no longer declares `programFines` or `programCosts`;
9
+ `late_charge` routes fines and recovery through its `fines_to` and `costs_to`
10
+ party tunables. `account(owner, cash, external)` is an ordinary keyed account
11
+ instead of a refusal. The package stops generating the header and sample
12
+ index pages and the two llms text files; `hsx headers` prints the instrument
13
+ inventory.
14
+
15
+ # HSX 5.8.0
16
+
17
+ The checker names misspelled fields, parties and states at the misspelled word,
18
+ with the nearest declared name: "`amont` is not a field or party of `deposit`.
19
+ Did you mean `amount`?" This covers moves, actor parties, and field paths in
20
+ `requires`, `unique ... on`, `set`, `calculate:`, `invoke`, `invariants` and the
21
+ `at:` of `due` and `deadline`. The check runs once on the lowered program, so
22
+ it also names a wrong segment deep in a path ("`balanse` is not a field of
23
+ `self.held`"), `input.x` and `party.x` names, invoke instruments, actions and
24
+ inputs, selection instruments, `where` fields and references, `ref<...>`
25
+ targets and the list of an `at` calculation. Swapped adjacent letters count as
26
+ one edit when picking the suggestion. A state typo in `from`, `to` or `initial` is one error
27
+ instead of an invalid transition plus every unreachable state. A program
28
+ instrument attached before its declaration reports each mistake once.
29
+
30
+ A `ref<T>[]` binding accepts a list of sibling attachments, so `on: [plan]`
31
+ compiles to the same program as `on: plan`.
32
+
1
33
  # HSX 5.7.0
2
34
 
3
35
  Follows UDL 4.9.0. Object `entryActions` admits enrolled customers through named
package/README.md CHANGED
@@ -21,8 +21,8 @@ Start with the [first program and rental guide](docs/README.md). Save the
21
21
  It holds 1,000 SAR, charges a one-time 50 SAR late fee, and returns the remainder.
22
22
  The guide explains the fixed business binding and the host needed to execute it.
23
23
 
24
- The [header inventory](docs/headers.md) lists available instruments and tunables.
25
- The [sample index](docs/examples.md) includes both complete action paths and
24
+ `npx hsx headers` prints every std instrument and its tunables from the bundled
25
+ headers. The [samples](examples/) include both complete action paths and
26
26
  compositions that still need child-record actions or adapter bindings. A compile
27
27
  pass does not prove that a flow is publicly executable or funded.
28
28
 
@@ -56,6 +56,5 @@ Run `bun bin/hsx.ts check examples/rental-deposit.hsx` to use the source CLI.
56
56
 
57
57
  Edit prose in `docs/README.md`, programs in `examples/`, and header comments in
58
58
  `std/`. Run `bun run generate` after changing these inputs. It emits the header
59
- and example bundles, reference indexes and model-readable files. Do not edit
60
- `docs/headers.md`, `docs/examples.md`, `llms.txt` or `llms-full.txt` by hand.
59
+ and example bundles.
61
60
  `bun run build` also builds the browser playground.
package/bin/hsx.ts CHANGED
@@ -1,9 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * The `hsx` executable. Everything it does lives in `runCli`; this file only
4
- * binds that function to the real filesystem and the real streams.
5
- */
6
-
7
2
  import { readFile, writeFile } from "node:fs/promises";
8
3
  import { runCli } from "../src/cli.ts";
9
4
 
package/dist/bin/hsx.d.ts CHANGED
@@ -1,7 +1,3 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * The `hsx` executable. Everything it does lives in `runCli`; this file only
4
- * binds that function to the real filesystem and the real streams.
5
- */
6
2
  export {};
7
3
  //# sourceMappingURL=hsx.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"hsx.d.ts","sourceRoot":"","sources":["../../bin/hsx.ts"],"names":[],"mappings":";AACA;;;GAGG"}
1
+ {"version":3,"file":"hsx.d.ts","sourceRoot":"","sources":["../../bin/hsx.ts"],"names":[],"mappings":""}
package/dist/bin/hsx.js CHANGED
@@ -1,8 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /**
3
- * The `hsx` executable. Everything it does lives in `runCli`; this file only
4
- * binds that function to the real filesystem and the real streams.
5
- */
6
2
  import { readFile, writeFile } from "node:fs/promises";
7
3
  import { runCli } from "../src/cli.js";
8
4
  process.exitCode = await runCli(process.argv.slice(2), {
@@ -1 +1 @@
1
- {"version":3,"file":"hsx.js","sourceRoot":"","sources":["../../bin/hsx.ts"],"names":[],"mappings":";AACA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,OAAO,CAAC,QAAQ,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACrD,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC;IAChD,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAC1C,SAAS,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC;CACjE,CAAC,CAAC"}
1
+ {"version":3,"file":"hsx.js","sourceRoot":"","sources":["../../bin/hsx.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAEvC,OAAO,CAAC,QAAQ,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;IACrD,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC;IAChD,GAAG,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC;IAChD,QAAQ,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAC1C,SAAS,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,CAAC,SAAS,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC;CACjE,CAAC,CAAC"}
@@ -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,CAmmHf"}
@@ -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",
@@ -356,8 +361,6 @@ export function compile(source, options = {}) {
356
361
  parties: Object.assign(Object.create(null), {
357
362
  programOperator: { kind: "business", role: "program_operator" },
358
363
  programTax: { kind: "business", role: "tax_payable" },
359
- programFines: { kind: "business", role: "fine_payable" },
360
- programCosts: { kind: "business", role: "cost_recovery" },
361
364
  }),
362
365
  objects: [],
363
366
  instruments: [],
@@ -403,6 +406,10 @@ export function compile(source, options = {}) {
403
406
  }
404
407
  }
405
408
  const origins = [];
409
+ // Lowered objects lose their spans, so each keeps the expression that
410
+ // produced it for the name check after lowering.
411
+ const authored = new WeakMap();
412
+ const declarations = new WeakMap();
406
413
  const exposures = [];
407
414
  const refundBindings = new Map();
408
415
  const resolveFamily = (rawPath, expr, required = true) => {
@@ -807,7 +814,8 @@ export function compile(source, options = {}) {
807
814
  continue;
808
815
  const t = param.value.kind === "default" ? param.value.type : param.value;
809
816
  const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
810
- let v = (supplied.has(param.key) && !attachmentInfo) || type === "enum"
817
+ const asWritten = (supplied.has(param.key) && !attachmentInfo) || type === "enum";
818
+ let v = asWritten
811
819
  ? actual
812
820
  : resolve(actual, new Set(), !!attachmentInfo && type === "party");
813
821
  if (type === "duration" &&
@@ -843,7 +851,15 @@ export function compile(source, options = {}) {
843
851
  : { party: v.value };
844
852
  }
845
853
  else if (type === "ref") {
846
- const values = v.kind === "list" ? v.items : [v];
854
+ // Listed objects resolve like a single binding, so `on: [plan]`
855
+ // names the sibling attachment that `on: plan` names.
856
+ const values = v.kind !== "list"
857
+ ? [v]
858
+ : asWritten
859
+ ? v.items
860
+ : v.items.map((item) => resolve(item));
861
+ if (v.kind === "list")
862
+ environment.set(param.key, { ...v, items: values });
847
863
  if (v.kind === "list" && (t.kind !== "type" || !t.many))
848
864
  fail(v, `${param.key} accepts one reference`, "use one object name");
849
865
  if (!values.length || values.length > 16)
@@ -1093,9 +1109,11 @@ export function compile(source, options = {}) {
1093
1109
  };
1094
1110
  const val = (expr) => {
1095
1111
  const v = resolve(expr);
1096
- return v.kind === "name" && !["true", "false"].includes(v.value)
1097
- ? { field: path(v) }
1098
- : { literal: literal(v) };
1112
+ if (v.kind !== "name" || ["true", "false"].includes(v.value))
1113
+ return { literal: literal(v) };
1114
+ const value = { field: path(v) };
1115
+ authored.set(value, expr);
1116
+ return value;
1099
1117
  };
1100
1118
  const data = (expr) => {
1101
1119
  if (expr.kind === "call" &&
@@ -1139,6 +1157,7 @@ export function compile(source, options = {}) {
1139
1157
  continue;
1140
1158
  result[k] = data(v);
1141
1159
  }
1160
+ authored.set(result, value);
1142
1161
  return result;
1143
1162
  }
1144
1163
  const result = Object.fromEntries([...rawEntries].map(([key, value]) => [key, data(value)]));
@@ -1160,10 +1179,14 @@ export function compile(source, options = {}) {
1160
1179
  };
1161
1180
  return null;
1162
1181
  }
1182
+ authored.set(result, value);
1163
1183
  return result;
1164
1184
  }
1165
- if (value.kind === "list")
1166
- return value.items.map(data).filter((item) => item !== null);
1185
+ if (value.kind === "list") {
1186
+ const items = value.items.map(data).filter((item) => item !== null);
1187
+ authored.set(items, value);
1188
+ return items;
1189
+ }
1167
1190
  if (value.kind === "call" &&
1168
1191
  value.name === "child" &&
1169
1192
  value.args.length === 2) {
@@ -1277,8 +1300,6 @@ export function compile(source, options = {}) {
1277
1300
  f.book = t.args[1] ? text(t.args[1]) : "cash";
1278
1301
  if (t.args[2]) {
1279
1302
  const mode = text(t.args[2]);
1280
- if (mode === "external")
1281
- fail(t, "external account mode was removed", "use a reservation and instruction-bound evidence");
1282
1303
  if (mode === "contra")
1283
1304
  f.contra = true;
1284
1305
  else
@@ -1370,6 +1391,7 @@ export function compile(source, options = {}) {
1370
1391
  });
1371
1392
  else
1372
1393
  fail(constant, `unknown calculation ${constant.name}`, "use rate, sum, minimum, subtract, multiply, divide, before or after");
1394
+ authored.set(calculations.at(-1), constant);
1373
1395
  }
1374
1396
  else if (type === "enum" && constant.kind === "name")
1375
1397
  f.value = constant.value;
@@ -1382,12 +1404,25 @@ export function compile(source, options = {}) {
1382
1404
  else
1383
1405
  f.value = literal(constant, String(type));
1384
1406
  }
1407
+ authored.set(f, row.value);
1385
1408
  result.push(f);
1386
1409
  }
1387
1410
  return result;
1388
1411
  };
1389
1412
  fields.push(...lowerFields(asBlock(body.get("fields"))));
1390
- const lifecycle = data(asBlock(body.get("lifecycle")));
1413
+ const lifecycleBlock = asBlock(body.get("lifecycle"));
1414
+ const lifecycle = data(lifecycleBlock);
1415
+ const states = Array.isArray(lifecycle.states) ? lifecycle.states : [];
1416
+ const checkState = (expr, state) => {
1417
+ if (typeof state !== "string" || !states.length)
1418
+ return;
1419
+ if (states.includes(state))
1420
+ return;
1421
+ fail(expr, `\`${state}\` is not a state of \`${decl.name}\`.${didYouMean(state, states)}`, `Use one of ${states.join(", ")}, or add \`${state}\` to \`lifecycle.states\`.`);
1422
+ };
1423
+ const initialExpr = entries(lifecycleBlock).get("initial");
1424
+ if (initialExpr)
1425
+ checkState(initialExpr, lifecycle.initial);
1391
1426
  lifecycle.transitions = Object.create(null);
1392
1427
  const inst = {
1393
1428
  id,
@@ -1656,6 +1691,7 @@ export function compile(source, options = {}) {
1656
1691
  moves: [],
1657
1692
  ...(actionSubject ? { subject: actionSubject } : {}),
1658
1693
  };
1694
+ authored.set(a, row.value);
1659
1695
  for (const binding of boundaryBindings)
1660
1696
  if (!a.subject?.adapters.find((entry) => entry.binding === binding)
1661
1697
  ?.snapshot)
@@ -1667,10 +1703,13 @@ export function compile(source, options = {}) {
1667
1703
  const to = slots.get("to");
1668
1704
  if (!from || !to)
1669
1705
  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
- };
1706
+ const sources = from.kind === "list" ? from.items : [from];
1707
+ const fromStates = sources.map((e) => String(data(e)));
1708
+ const toState = String(data(to));
1709
+ sources.forEach((e, index) => checkState(e, fromStates[index]));
1710
+ if (toState !== "preserve")
1711
+ checkState(to, toState);
1712
+ lifecycle.transitions[name] = { from: fromStates, to: toState };
1674
1713
  }
1675
1714
  for (const [key, expr] of slots) {
1676
1715
  if (["from", "to", "input", "summary", "subject"].includes(key))
@@ -1678,6 +1717,7 @@ export function compile(source, options = {}) {
1678
1717
  if (key === "moves") {
1679
1718
  const moves = expr.kind === "list" ? expr.items : [expr];
1680
1719
  for (const [index, move] of moves.entries()) {
1720
+ const lowered = a.moves.length;
1681
1721
  const parts = entries(asBlock(move));
1682
1722
  const op = parts.has("operation")
1683
1723
  ? String(data(parts.get("operation")))
@@ -1831,6 +1871,8 @@ export function compile(source, options = {}) {
1831
1871
  }
1832
1872
  if (total !== 10000)
1833
1873
  fail(move, "split percentages must sum to 100%", "give the recipients exactly 100% together");
1874
+ for (const item of a.moves.slice(lowered))
1875
+ authored.set(item, move);
1834
1876
  continue;
1835
1877
  }
1836
1878
  if (!amount || !from || !to)
@@ -1972,6 +2014,8 @@ export function compile(source, options = {}) {
1972
2014
  });
1973
2015
  else
1974
2016
  fail(move, `unknown transfer instruction ${op}`, "choose create, reserve, post, or void from internal_transfer");
2017
+ for (const item of a.moves.slice(lowered))
2018
+ authored.set(item, move);
1975
2019
  }
1976
2020
  }
1977
2021
  else
@@ -2090,6 +2134,7 @@ export function compile(source, options = {}) {
2090
2134
  };
2091
2135
  }
2092
2136
  document.instruments.push(inst);
2137
+ declarations.set(inst, decl);
2093
2138
  for (const [key, definition] of records) {
2094
2139
  const childAttachment = attachmentInfo
2095
2140
  ? {
@@ -2337,7 +2382,7 @@ export function compile(source, options = {}) {
2337
2382
  if (bindingDiagnostics.length)
2338
2383
  return {
2339
2384
  verdict: "invalid",
2340
- diagnostics: bindingDiagnostics
2385
+ diagnostics: distinct(bindingDiagnostics)
2341
2386
  .sort((a, b) => a.span.start - b.span.start)
2342
2387
  .map((d) => diagnostic(d, "check")),
2343
2388
  };
@@ -2521,6 +2566,220 @@ export function compile(source, options = {}) {
2521
2566
  failWithCode(objects.get(kind.id), "subject_field_unknown", `column ${column} is unknown on ${kind.id}`, "name an authored field or attached requirement");
2522
2567
  }
2523
2568
  }
2569
+ const byId = new Map(document.instruments.map((inst) => [inst.id, inst]));
2570
+ const site = (owner, key, fallback) => {
2571
+ const expr = authored.get(owner);
2572
+ if (expr?.kind === "block")
2573
+ return expr.entries.find((entry) => entry.key === key)?.value ?? expr;
2574
+ if (expr?.kind === "list" && Array.isArray(owner))
2575
+ return expr.items.length === owner.length
2576
+ ? expr.items[Number(key)]
2577
+ : expr;
2578
+ return expr ?? fallback;
2579
+ };
2580
+ const keySite = (owner, key, fallback) => {
2581
+ const expr = authored.get(owner);
2582
+ const entry = expr?.kind === "block"
2583
+ ? expr.entries.find((item) => item.key === key)
2584
+ : undefined;
2585
+ if (!entry)
2586
+ return fallback;
2587
+ return {
2588
+ ...entry,
2589
+ span: { start: entry.span.start, end: entry.span.start + key.length },
2590
+ };
2591
+ };
2592
+ const builtins = (inst) => [
2593
+ "id",
2594
+ "now",
2595
+ "createdAt",
2596
+ "productRevision",
2597
+ "status",
2598
+ ...(inst.subject ? ["subject"] : []),
2599
+ ];
2600
+ const parties = (inst) => [
2601
+ ...Object.keys(document.parties),
2602
+ ...(inst.subject ? subjectPartyRoles : []),
2603
+ ];
2604
+ const members = (field) => {
2605
+ if (field.type === "account")
2606
+ return ["balance", "reserved"];
2607
+ if (field.type !== "ref")
2608
+ return [];
2609
+ const [id] = [field.target].flat();
2610
+ const target = field.targetKind === "object"
2611
+ ? document.objects.find((object) => object.id === id)
2612
+ : byId.get(id);
2613
+ if (!target)
2614
+ return [];
2615
+ return [
2616
+ ...target.fields.map((item) => item.name),
2617
+ ...("lifecycle" in target ? builtins(target) : []),
2618
+ ];
2619
+ };
2620
+ const checkParty = (name, at, inst) => {
2621
+ const names = parties(inst);
2622
+ if (!names.includes(name))
2623
+ fail(at, `\`${name}\` is not a declared party.${didYouMean(name, names)}`, `Declare it with \`party ${name}: business\`, or name a declared party (${names.join(", ")}).`);
2624
+ };
2625
+ const checkInput = (name, at, label, input) => {
2626
+ const names = input.map((field) => field.name);
2627
+ if (!names.includes(name))
2628
+ 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(", ")}` : ""}.`);
2629
+ };
2630
+ const checkTarget = (id, at, objects = false) => {
2631
+ const ids = [
2632
+ ...byId.keys(),
2633
+ ...(objects ? document.objects.map((object) => object.id) : []),
2634
+ ];
2635
+ const noun = objects ? "instrument or object" : "instrument";
2636
+ if (!ids.includes(id))
2637
+ fail(at, `\`${id}\` is not a declared ${noun}.${didYouMean(id, ids)}`, `Name a declared ${noun}.`);
2638
+ };
2639
+ const checkPath = (path, at, scope) => {
2640
+ const { inst, action } = scope;
2641
+ const parts = path.split(".");
2642
+ const found = (count) => resolveField(document, inst, parts.slice(0, count).join("."), action?.input, action);
2643
+ // Subject paths keep their own check against the action's requirements.
2644
+ if (found(parts.length) || parts[0] === "subject")
2645
+ return;
2646
+ if (parts[0] === "party" && parts[1]) {
2647
+ checkParty(parts[1], at, inst);
2648
+ if (!found(2))
2649
+ return;
2650
+ }
2651
+ if (!["self", "input", "party"].includes(parts[0]))
2652
+ parts.unshift("self");
2653
+ let count = 2;
2654
+ while (count <= parts.length && found(count))
2655
+ count++;
2656
+ if (count > parts.length)
2657
+ return;
2658
+ const name = parts[count - 1];
2659
+ if (count > 2) {
2660
+ const prefix = parts.slice(0, count - 1).join(".");
2661
+ const names = members(found(count - 1));
2662
+ fail(at, `\`${name}\` is not a field of \`${prefix}\`.${didYouMean(name, names)}`, names.length
2663
+ ? `Use one of ${names.join(", ")}.`
2664
+ : `\`${prefix}\` has no fields.`);
2665
+ }
2666
+ if (parts[0] === "input")
2667
+ return checkInput(name, at, scope.label, action?.input ?? []);
2668
+ const declared = declarations.get(inst).name;
2669
+ const fields = inst.fields.map((field) => field.name);
2670
+ 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(", ")}).`);
2671
+ };
2672
+ const scopeOf = (inst) => ({
2673
+ inst,
2674
+ label: declarations.get(inst).name,
2675
+ });
2676
+ const checkSelection = (selection, at, scope) => {
2677
+ walk(selection.anchor, "anchor", site(selection, "anchor", at), scope);
2678
+ walk(selection.where, "where", site(selection, "where", at), scope);
2679
+ for (const id of [selection.instrument].flat()) {
2680
+ checkTarget(id, site(selection, "instrument", at));
2681
+ const target = scopeOf(byId.get(id));
2682
+ const member = (key, where) => checkPath(key.startsWith("self.") ? key : `self.${key}`, where, target);
2683
+ for (const key of Object.keys(selection.where ?? {}))
2684
+ member(key, keySite(selection.where, key, at));
2685
+ member(selection.reference, site(selection, "reference", at));
2686
+ selection.order?.forEach((key, index) => member(key, site(selection.order, index, at)));
2687
+ if (selection.window)
2688
+ member(selection.window.field, site(selection.window, "field", at));
2689
+ }
2690
+ };
2691
+ const checkCall = (call, at, scope) => {
2692
+ const bind = "range" in call ? call.range?.bind : undefined;
2693
+ for (const [key, value] of Object.entries(call))
2694
+ if (key !== "input")
2695
+ walk(value, key, site(call, key, at), scope);
2696
+ for (const [key, value] of Object.entries(call.input))
2697
+ if (!("field" in value) || value.field !== bind)
2698
+ walk(value, key, site(call.input, key, at), scope);
2699
+ if ("instrument" in call)
2700
+ checkTarget(call.instrument, site(call, "instrument", at));
2701
+ const reference = "reference" in call
2702
+ ? resolveField(document, scope.inst, call.reference, scope.action?.input, scope.action)
2703
+ : undefined;
2704
+ const ids = "instrument" in call
2705
+ ? [call.instrument]
2706
+ : "selection" in call
2707
+ ? [call.selection.instrument].flat()
2708
+ : reference?.type === "ref" && reference.targetKind === "instrument"
2709
+ ? [reference.target].flat()
2710
+ : [];
2711
+ for (const id of ids) {
2712
+ const target = byId.get(id);
2713
+ if (!target)
2714
+ continue;
2715
+ const action = target.actions[call.action];
2716
+ if (!action)
2717
+ fail(site(call, "action", at), `\`${call.action}\` is not an action of \`${id}\`.${didYouMean(call.action, target.actionOrder)}`, `Use one of ${target.actionOrder.join(", ")}.`);
2718
+ for (const key of Object.keys(call.input))
2719
+ checkInput(key, keySite(call.input, key, at), `${id}.${call.action}`, action.input);
2720
+ }
2721
+ };
2722
+ const pathKeys = [
2723
+ "field",
2724
+ "reference",
2725
+ "anchor",
2726
+ "at",
2727
+ "list",
2728
+ "from",
2729
+ "to",
2730
+ "transfer",
2731
+ "subject",
2732
+ "instruction",
2733
+ "installment",
2734
+ "recipient",
2735
+ "dueAt",
2736
+ "fields",
2737
+ ];
2738
+ const walk = (value, key, at, scope) => {
2739
+ if (typeof value === "string") {
2740
+ if (key === "party")
2741
+ checkParty(value, at, scope.inst);
2742
+ if (key === "target")
2743
+ checkPath(`self.${value}`, at, scope);
2744
+ if (pathKeys.includes(key))
2745
+ checkPath(value, at, scope);
2746
+ return;
2747
+ }
2748
+ if (!value || typeof value !== "object")
2749
+ return;
2750
+ if (Array.isArray(value))
2751
+ return value.forEach((item, index) => walk(item, key, site(value, index, at), scope));
2752
+ if (key === "selection")
2753
+ return checkSelection(value, at, scope);
2754
+ if (key === "invoke")
2755
+ return checkCall(value, at, scope);
2756
+ if (key === "set")
2757
+ for (const name of Object.keys(value))
2758
+ checkPath(`self.${name}`, keySite(value, name, at), scope);
2759
+ for (const [child, item] of Object.entries(value))
2760
+ walk(item, child, site(value, child, at), scope);
2761
+ };
2762
+ for (const inst of document.instruments) {
2763
+ const decl = declarations.get(inst);
2764
+ const scope = scopeOf(inst);
2765
+ for (const field of [
2766
+ ...inst.fields,
2767
+ ...Object.values(inst.actions).flatMap((action) => action.input),
2768
+ ])
2769
+ if ("target" in field && field.target !== undefined)
2770
+ for (const id of [field.target].flat())
2771
+ checkTarget(id, site(field, "target", decl), true);
2772
+ walk(inst.calculate, "calculate", decl, scope);
2773
+ walk(inst.invariants, "invariants", decl, scope);
2774
+ for (const [name, action] of Object.entries(inst.actions))
2775
+ for (const [key, value] of Object.entries(action))
2776
+ if (key !== "input" && key !== "subject")
2777
+ walk(value, key, site(action, key, decl), {
2778
+ inst,
2779
+ label: `${decl.name}.${name}`,
2780
+ action,
2781
+ });
2782
+ }
2524
2783
  const eliminatedStates = new Map();
2525
2784
  // Remove branches excluded by immutable tunables before checking reference states.
2526
2785
  for (const inst of document.instruments) {
@@ -2628,22 +2887,6 @@ export function compile(source, options = {}) {
2628
2887
  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");
2629
2888
  }
2630
2889
  }
2631
- const usedParties = new Set();
2632
- const collectParties = (value) => {
2633
- if (typeof value === "string") {
2634
- usedParties.add(value);
2635
- if (value.startsWith("party."))
2636
- usedParties.add(value.split(".")[1]);
2637
- }
2638
- else if (Array.isArray(value))
2639
- value.forEach(collectParties);
2640
- else if (value && typeof value === "object")
2641
- Object.values(value).forEach(collectParties);
2642
- };
2643
- collectParties(document.instruments);
2644
- for (const party of ["programFines", "programCosts"])
2645
- if (!usedParties.has(party) && !names.has(party))
2646
- delete document.parties[party];
2647
2890
  const validated = validateUdl(document);
2648
2891
  if (!validated.ok)
2649
2892
  return {
@@ -2702,7 +2945,7 @@ export function compile(source, options = {}) {
2702
2945
  if (error instanceof CompileFailure)
2703
2946
  return {
2704
2947
  verdict: "invalid",
2705
- diagnostics: [...bindingDiagnostics, error.diagnostic].map((d) => diagnostic(d, "check")),
2948
+ diagnostics: distinct([...bindingDiagnostics, error.diagnostic]).map((d) => diagnostic(d, "check")),
2706
2949
  };
2707
2950
  throw error;
2708
2951
  }