@hyperscale0/hsx 3.2.0 → 4.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +1 -1
  3. package/dist/src/ast.d.ts +17 -2
  4. package/dist/src/ast.d.ts.map +1 -1
  5. package/dist/src/ast.js.map +1 -1
  6. package/dist/src/cli.js +1 -1
  7. package/dist/src/compile.d.ts +6 -0
  8. package/dist/src/compile.d.ts.map +1 -1
  9. package/dist/src/compile.js +1194 -97
  10. package/dist/src/compile.js.map +1 -1
  11. package/dist/src/cost.d.ts +1 -1
  12. package/dist/src/cost.d.ts.map +1 -1
  13. package/dist/src/cost.js +52 -9
  14. package/dist/src/cost.js.map +1 -1
  15. package/dist/src/headers.d.ts +2 -2
  16. package/dist/src/headers.d.ts.map +1 -1
  17. package/dist/src/headers.js +3 -2
  18. package/dist/src/headers.js.map +1 -1
  19. package/dist/src/index.d.ts +3 -2
  20. package/dist/src/index.d.ts.map +1 -1
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/src/lex.d.ts +1 -1
  23. package/dist/src/lex.d.ts.map +1 -1
  24. package/dist/src/lex.js +5 -0
  25. package/dist/src/lex.js.map +1 -1
  26. package/dist/src/parse.js +91 -5
  27. package/dist/src/parse.js.map +1 -1
  28. package/dist/src/std-bundle.d.ts.map +1 -1
  29. package/dist/src/std-bundle.js +10 -9
  30. package/dist/src/std-bundle.js.map +1 -1
  31. package/dist/src/version.d.ts +2 -2
  32. package/dist/src/version.js +2 -2
  33. package/docs/README.md +51 -27
  34. package/docs/headers.md +43 -40
  35. package/examples/cost-table.json +40 -324
  36. package/examples/library.hsx +12 -57
  37. package/package.json +7 -5
  38. package/src/ast.ts +11 -1
  39. package/src/cli.ts +1 -1
  40. package/src/compile.ts +1630 -114
  41. package/src/cost.ts +60 -16
  42. package/src/headers.ts +3 -2
  43. package/src/index.ts +12 -1
  44. package/src/lex.ts +5 -0
  45. package/src/parse.ts +87 -5
  46. package/src/std-bundle.ts +10 -9
  47. package/src/version.ts +2 -2
  48. package/std/approvals.hsx +3 -3
  49. package/std/collections.hsx +45 -4
  50. package/std/escrow.hsx +17 -8
  51. package/std/financing.hsx +292 -42
  52. package/std/insurance.hsx +4 -5
  53. package/std/lending.hsx +7 -8
  54. package/std/marketplace.hsx +90 -6
  55. package/std/money.hsx +15 -49
  56. package/std/reporting.hsx +1286 -0
  57. package/std/travel.hsx +5 -6
@@ -1,5 +1,6 @@
1
+ import { hash as sha256 } from "fast-sha256";
1
2
  import { buildUdlCostManifest } from "./cost.js";
2
- import { validateUdl, } from "@hyperscale0/udl";
3
+ import { validateUdl, resolveField, sameObjectField, subjectPartyRoles, RESERVED_OBJECT_NAMES, udlObjectFieldSchema, } from "@hyperscale0/udl";
3
4
  import { tunableBounds } from "./tunables.js";
4
5
  import { parseProgram } from "./parse.js";
5
6
  import { lineColAt, } from "./ast.js";
@@ -12,7 +13,125 @@ class CompileFailure extends Error {
12
13
  }
13
14
  }
14
15
  function fail(expr, message, fix) {
15
- throw new CompileFailure({ code: "HSX1001", message, fix, span: expr.span });
16
+ return failWithCode(expr, "HSX1001", message, fix);
17
+ }
18
+ function failWithCode(expr, code, message, fix) {
19
+ throw new CompileFailure({
20
+ code,
21
+ message,
22
+ fix,
23
+ span: expr.span,
24
+ ...(expr.source ? { source: expr.source } : {}),
25
+ });
26
+ }
27
+ function lowerFieldShape(row, resolveExpr = (e) => e) {
28
+ let rawValue = row.value.kind === "default" ? row.value.type : row.value;
29
+ let isSensitive = false;
30
+ if (rawValue.kind === "call" && rawValue.name === "sensitive") {
31
+ isSensitive = true;
32
+ rawValue = rawValue.args[0];
33
+ }
34
+ const t = rawValue;
35
+ if (t.kind === "block") {
36
+ const b = entries(t);
37
+ if (b.has("family") || b.has("target") || b.has("instrument")) {
38
+ const tgt = b.get("instrument") ?? b.get("target");
39
+ let target;
40
+ if (tgt) {
41
+ const resolved = resolveExpr(tgt);
42
+ if (resolved.kind === "list") {
43
+ const items = resolved.items.map((i) => text(resolveExpr(i)));
44
+ target = items.length === 1 ? items[0] : items;
45
+ }
46
+ else {
47
+ target = text(resolved);
48
+ }
49
+ }
50
+ return {
51
+ name: row.key,
52
+ type: "ref",
53
+ targetKind: "instrument",
54
+ ...(target !== undefined ? { target } : {}),
55
+ ...(isSensitive ? { sensitive: true } : {}),
56
+ };
57
+ }
58
+ }
59
+ const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
60
+ const f = {
61
+ name: row.key,
62
+ type,
63
+ ...(t.kind === "type" && t.optional ? { optional: true } : {}),
64
+ ...(isSensitive ? { sensitive: true } : {}),
65
+ };
66
+ if (type === "enum" && t.kind === "call") {
67
+ f.values = t.args.map(text);
68
+ }
69
+ if (type === "text" && t.kind === "call") {
70
+ if (t.args.length < 2 || t.args.length > 3) {
71
+ fail(t, "bounded text needs length bounds and an optional pattern", "write text(1, 80)");
72
+ }
73
+ f.minLength = literal(resolveExpr(t.args[0]));
74
+ f.maxLength = literal(resolveExpr(t.args[1]));
75
+ if (t.args[2])
76
+ f.pattern = literal(resolveExpr(t.args[2]));
77
+ }
78
+ if (["integer", "money"].includes(type) && t.kind === "call") {
79
+ if (t.args.length !== 2) {
80
+ fail(t, "bounded fields need a minimum and maximum", "write integer(1, 12) or money(0 SAR, 100 SAR)");
81
+ }
82
+ f.minimum = literal(resolveExpr(t.args[0]));
83
+ f.maximum = literal(resolveExpr(t.args[1]));
84
+ }
85
+ if (type === "list" && t.kind === "call") {
86
+ const item = t.args[0];
87
+ f.item = item.kind === "type" ? item.name : text(item);
88
+ if (item.kind === "type" && item.name === "ref") {
89
+ f.target = item.target;
90
+ f.targetKind = "object";
91
+ }
92
+ f.maxItems = t.args[1] ? literal(resolveExpr(t.args[1])) : 366;
93
+ }
94
+ if (type === "list" && t.kind === "type") {
95
+ f.item = t.target;
96
+ f.maxItems = 366;
97
+ }
98
+ if (type === "ref") {
99
+ const target = t.kind === "type" ? t.target : f.target;
100
+ if (!target && t.kind !== "block") {
101
+ fail(row, "reference needs a target", "write ref<object>");
102
+ }
103
+ f.targetKind = f.targetKind ?? "object";
104
+ if (target)
105
+ f.target = target;
106
+ }
107
+ return f;
108
+ }
109
+ function lowerObjectField(row, resolveExpr = (e) => e) {
110
+ const f = lowerFieldShape(row, resolveExpr);
111
+ const constant = row.value.kind === "default" ? resolveExpr(row.value.value) : undefined;
112
+ if (constant && !(constant.kind === "name" && constant.value === "runtime")) {
113
+ f.value =
114
+ f.type === "enum" && constant.kind === "name"
115
+ ? constant.value
116
+ : literal(constant);
117
+ }
118
+ const result = udlObjectFieldSchema.safeParse(f);
119
+ if (!result.success)
120
+ fail(row, result.error.message, "use a UDL object field type and its constraints");
121
+ return result.data;
122
+ }
123
+ function canonicalJson(value) {
124
+ if (value === null || typeof value !== "object") {
125
+ return JSON.stringify(value);
126
+ }
127
+ if (Array.isArray(value)) {
128
+ return `[${value.map((item) => (item === undefined ? "null" : canonicalJson(item))).join(",")}]`;
129
+ }
130
+ const record = value;
131
+ const keys = Object.keys(record)
132
+ .filter((k) => record[k] !== undefined)
133
+ .sort();
134
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(record[k])}`).join(",")}}`;
16
135
  }
17
136
  const emptyBlock = {
18
137
  kind: "block",
@@ -28,7 +147,14 @@ function entries(block) {
28
147
  for (const row of block.entries) {
29
148
  const previous = map.get(row.key);
30
149
  if (previous) {
31
- if (!["requires", "moves", "invariants", "invoke", "calculate"].includes(row.key))
150
+ if (![
151
+ "requires",
152
+ "moves",
153
+ "invariants",
154
+ "invoke",
155
+ "calculate",
156
+ "expose",
157
+ ].includes(row.key))
32
158
  fail(row, `duplicate ${row.key}`, "keep one value for this name");
33
159
  const items = (expr) => expr.kind === "list" ? expr.items : [expr];
34
160
  map.set(row.key, {
@@ -126,9 +252,10 @@ function literal(expr) {
126
252
  }
127
253
  export function compile(source, options = {}) {
128
254
  const parsed = parseProgram(source);
255
+ const sources = new Map([["program", source]]);
129
256
  const diagnostic = (d, stage) => ({
130
257
  ...d,
131
- ...lineColAt(source, d.span.start),
258
+ ...lineColAt(sources.get(d.source ?? "program") ?? source, d.span.start),
132
259
  severity: "error",
133
260
  stage,
134
261
  });
@@ -144,6 +271,9 @@ export function compile(source, options = {}) {
144
271
  if (program.currency !== "SAR")
145
272
  fail(program, "this release supports SAR", "write currency SAR or omit currency");
146
273
  const templates = new Map();
274
+ const declarationSources = new Map();
275
+ const declarationExportPaths = new Map();
276
+ const requirementOrigins = new Map();
147
277
  const used = new Set();
148
278
  for (const use of program.decls.filter((d) => d.kind === "use")) {
149
279
  if (used.has(use.name))
@@ -152,17 +282,38 @@ export function compile(source, options = {}) {
152
282
  const content = (options.standardLibrary ?? bundledStandardLibrary).source(use.name);
153
283
  if (!content)
154
284
  fail(use, `unknown header ${use.name}`, "choose a published header");
285
+ sources.set(use.name, content);
155
286
  const header = parseProgram(content);
156
287
  if (header.diagnostics.length ||
157
288
  !header.program.header ||
158
289
  header.program.name !== use.name)
159
290
  fail(use, `header ${use.name} is malformed`, "repair the header source before compiling");
291
+ const registerTemplates = (parentDecl, prefix, exportPath) => {
292
+ templates.set(prefix, parentDecl);
293
+ declarationSources.set(parentDecl, use.name);
294
+ declarationExportPaths.set(parentDecl, exportPath);
295
+ const recs = entries(asBlock(entries(parentDecl.body).get("records")));
296
+ for (const [recName, recBlock] of recs) {
297
+ const recDecl = {
298
+ kind: "instrument",
299
+ name: recName,
300
+ parameters: [],
301
+ body: asBlock(recBlock),
302
+ span: parentDecl.span,
303
+ };
304
+ registerTemplates(recDecl, `${prefix}.${recName}`, `${exportPath}.${recName}`);
305
+ }
306
+ };
160
307
  for (const decl of header.program.decls)
161
- if (decl.kind === "instrument")
162
- templates.set(`${use.name}.${decl.name}`, decl);
308
+ if (decl.kind === "instrument") {
309
+ registerTemplates(decl, `${use.name}.${decl.name}`, decl.name);
310
+ }
163
311
  }
312
+ for (const decl of program.decls)
313
+ if (decl.kind === "instrument")
314
+ templates.set(decl.name, decl);
164
315
  const document = {
165
- udl: 3,
316
+ udl: 4,
166
317
  version: 1,
167
318
  product: program.name,
168
319
  title: program.title,
@@ -173,9 +324,12 @@ export function compile(source, options = {}) {
173
324
  programFines: { kind: "business", role: "fine_payable" },
174
325
  programCosts: { kind: "business", role: "cost_recovery" },
175
326
  },
327
+ objects: [],
176
328
  instruments: [],
177
329
  };
178
330
  const objects = new Map();
331
+ const assignments = new Map();
332
+ const attachmentSubjects = new Map();
179
333
  const names = new Set();
180
334
  for (const decl of program.decls) {
181
335
  if (decl.kind === "expose" || decl.kind === "hide" || decl.kind === "use")
@@ -183,9 +337,28 @@ export function compile(source, options = {}) {
183
337
  if (names.has(decl.name))
184
338
  fail(decl, `duplicate declaration ${decl.name}`, "give this declaration a distinct name");
185
339
  names.add(decl.name);
186
- if (decl.kind === "object")
340
+ if (decl.kind === "object") {
187
341
  objects.set(decl.name, decl);
342
+ for (const entry of decl.body.entries) {
343
+ const match = /^attach\s+(\w+)\s*=\s*(.+)$/.exec(entry.key);
344
+ if (!match)
345
+ continue;
346
+ const name = `${decl.name}_${match[1]}`;
347
+ assignments.set(name, {
348
+ kind: "assignment",
349
+ name,
350
+ target: match[2],
351
+ body: asBlock(entry.value),
352
+ span: entry.span,
353
+ });
354
+ attachmentSubjects.set(name, decl.name);
355
+ }
356
+ }
357
+ if (decl.kind === "assignment")
358
+ assignments.set(decl.name, decl);
188
359
  if (decl.kind === "party") {
360
+ if (subjectPartyRoles.includes(decl.name))
361
+ failWithCode(decl, "party_name_reserved", `${decl.name} is a reserved subject role`, "choose a party name other than owner, actor or operator");
189
362
  if (!["person", "business", "staff"].includes(decl.partyKind))
190
363
  fail(decl, `unknown party kind ${decl.partyKind}`, "choose person, business, or staff");
191
364
  document.parties[decl.name] = {
@@ -197,7 +370,170 @@ export function compile(source, options = {}) {
197
370
  const origins = [];
198
371
  const materialApprovals = new Set();
199
372
  const implicitDecisions = new Map();
200
- const addInstrument = (decl, id, arguments_, origin, inherited = new Map(), inheritedApprovers = new Set(), inheritedEnums = new Map()) => {
373
+ const resolveFamily = (rawPath, expr, required = true) => {
374
+ const parts = rawPath.split(".");
375
+ if (parts.length < 2) {
376
+ if (!required)
377
+ return undefined;
378
+ failWithCode(expr, "HSX1001", `invalid family ${rawPath}`, "use module.instrument or module.instrument.record");
379
+ }
380
+ const moduleName = parts[0];
381
+ const exportPath = parts.slice(1).join(".");
382
+ const targetTemplate = templates.get(rawPath);
383
+ if (!targetTemplate) {
384
+ if (!required)
385
+ return undefined;
386
+ failWithCode(expr, "HSX1001", `unknown family declaration ${rawPath}`, "choose a declared standard instrument");
387
+ }
388
+ const topTemplate = templates.get(`${moduleName}.${parts[1]}`);
389
+ if (!topTemplate) {
390
+ if (!required)
391
+ return undefined;
392
+ failWithCode(expr, "HSX1001", `unknown family declaration ${moduleName}.${parts[1]}`, "choose a declared standard instrument");
393
+ }
394
+ const topBody = entries(topTemplate.body);
395
+ let revision;
396
+ if (topBody.has("familyRevision")) {
397
+ const val = literal(topBody.get("familyRevision"));
398
+ if (typeof val === "number")
399
+ revision = val;
400
+ }
401
+ let currentBody = topTemplate.body;
402
+ for (const recName of parts.slice(2)) {
403
+ const recs = entries(asBlock(entries(currentBody).get("records")));
404
+ const child = recs.get(recName);
405
+ if (child) {
406
+ currentBody = asBlock(child);
407
+ const cBody = entries(currentBody);
408
+ if (cBody.has("familyRevision")) {
409
+ const val = literal(cBody.get("familyRevision"));
410
+ if (typeof val === "number")
411
+ revision = val;
412
+ }
413
+ }
414
+ }
415
+ if (revision === undefined) {
416
+ if (!required)
417
+ return undefined;
418
+ failWithCode(expr, "HSX1001", `declaration ${rawPath} has no familyRevision declared`, "declare familyRevision on the standard instrument");
419
+ }
420
+ return { module: moduleName, exportPath, revision };
421
+ };
422
+ const addInstrument = (decl, id, arguments_, origin, inherited = new Map(), inheritedApprovers = new Set(), inheritedEnums = new Map(), attachmentInfo, familyDeclaration) => {
423
+ const resolveFamilyInstruments = (family, currentInstId, expr) => {
424
+ const found = new Set();
425
+ if (currentInstId && familyDeclaration) {
426
+ if (family.module === familyDeclaration.module &&
427
+ family.exportPath.startsWith(`${familyDeclaration.exportPath}.`)) {
428
+ const sub = family.exportPath
429
+ .slice(familyDeclaration.exportPath.length + 1)
430
+ .replaceAll(".", "_");
431
+ found.add(`${currentInstId}_${sub}`);
432
+ }
433
+ }
434
+ const topDeclName = family.exportPath.split(".")[0];
435
+ const subRecordPath = family.exportPath.includes(".")
436
+ ? family.exportPath.slice(topDeclName.length + 1).replaceAll(".", "_")
437
+ : undefined;
438
+ for (const asgn of assignments.values()) {
439
+ if (asgn.target === `${family.module}.${topDeclName}`) {
440
+ if (subRecordPath) {
441
+ found.add(`${asgn.name}_${subRecordPath}`);
442
+ }
443
+ else {
444
+ found.add(asgn.name);
445
+ }
446
+ }
447
+ }
448
+ for (const inst of document.instruments) {
449
+ if (inst.family &&
450
+ inst.family.module === family.module &&
451
+ inst.family.exportPath === family.exportPath &&
452
+ inst.family.revision === family.revision) {
453
+ found.add(inst.id);
454
+ }
455
+ }
456
+ const result = [...found];
457
+ if (result.length === 0 && expr) {
458
+ failWithCode(expr, "HSX1001", `no instruments found for family ${family.module}.${family.exportPath}`, "declare an attachment matching this family");
459
+ }
460
+ return result;
461
+ };
462
+ const resolveChildExportPath = (parentDecl, suffix) => {
463
+ const recs = entries(asBlock(entries(parentDecl.body).get("records")));
464
+ for (const [recName] of recs) {
465
+ if (suffix === recName)
466
+ return recName;
467
+ }
468
+ for (const [recName, recBlock] of recs) {
469
+ if (suffix.startsWith(`${recName}_`)) {
470
+ const rest = resolveChildExportPath({
471
+ kind: "instrument",
472
+ name: recName,
473
+ parameters: [],
474
+ body: asBlock(recBlock),
475
+ span: parentDecl.span,
476
+ }, suffix.slice(recName.length + 1));
477
+ if (rest)
478
+ return `${recName}.${rest}`;
479
+ }
480
+ }
481
+ return undefined;
482
+ };
483
+ const getInstrumentFamily = (targetId) => {
484
+ const existing = document.instruments.find((i) => i.id === targetId);
485
+ if (existing?.family)
486
+ return existing.family;
487
+ if (targetId.startsWith(`${id}_`) && familyDeclaration) {
488
+ const sub = resolveChildExportPath(decl, targetId.slice(id.length + 1));
489
+ if (sub) {
490
+ const fullExport = `${familyDeclaration.exportPath}.${sub}`;
491
+ try {
492
+ return resolveFamily(`${familyDeclaration.module}.${fullExport}`, { span: origin }, false);
493
+ }
494
+ catch { }
495
+ }
496
+ }
497
+ for (const [asgnName, asgn] of assignments) {
498
+ if (targetId === asgnName || targetId.startsWith(`${asgnName}_`)) {
499
+ const tmpl = templates.get(asgn.target);
500
+ if (!tmpl)
501
+ continue;
502
+ const mod = declarationSources.get(tmpl);
503
+ if (!mod || mod === "program")
504
+ continue;
505
+ if (targetId === asgnName) {
506
+ try {
507
+ return resolveFamily(asgn.target, asgn, false);
508
+ }
509
+ catch { }
510
+ }
511
+ else {
512
+ const sub = resolveChildExportPath(tmpl, targetId.slice(asgnName.length + 1));
513
+ if (sub) {
514
+ const fullTarget = `${asgn.target}.${sub}`;
515
+ try {
516
+ return resolveFamily(fullTarget, asgn, false);
517
+ }
518
+ catch { }
519
+ }
520
+ }
521
+ }
522
+ }
523
+ return undefined;
524
+ };
525
+ const checkTargetFamily = (targetIds, expectedFamily, expr) => {
526
+ const ids = Array.isArray(targetIds) ? targetIds : [targetIds];
527
+ for (const tid of ids) {
528
+ const fam = getInstrumentFamily(tid);
529
+ if (!fam ||
530
+ fam.module !== expectedFamily.module ||
531
+ fam.exportPath !== expectedFamily.exportPath ||
532
+ fam.revision !== expectedFamily.revision) {
533
+ failWithCode(expr, "HSX1001", `target instrument ${tid} family does not match expected family ${expectedFamily.module}.${expectedFamily.exportPath} (revision ${expectedFamily.revision})`, "ensure target instrument matches the declared family");
534
+ }
535
+ }
536
+ };
201
537
  const enums = new Map(inheritedEnums);
202
538
  for (const parameter of decl.parameters) {
203
539
  const type = parameter.value.kind === "default"
@@ -207,23 +543,44 @@ export function compile(source, options = {}) {
207
543
  enums.set(parameter.key, type.args.map(text));
208
544
  }
209
545
  const approvers = new Set(inheritedApprovers);
210
- const supplied = entries(arguments_);
546
+ const supplied = new Map();
547
+ for (const entry of arguments_.entries) {
548
+ if (supplied.has(entry.key))
549
+ fail(entry, `duplicate tunable ${entry.key}`, "supply each parameter once");
550
+ supplied.set(entry.key, entry.value);
551
+ }
211
552
  const environment = new Map(inherited);
212
553
  for (const param of decl.parameters) {
213
554
  const type = param.value.kind === "default" ? param.value.type : param.value;
214
555
  const fallback = param.value.kind === "default" ? param.value.value : undefined;
215
- const actual = supplied.get(param.key) ?? fallback;
556
+ const typeName = type.kind === "type" || type.kind === "call" ? type.name : text(type);
557
+ const partyParameter = attachmentInfo && (typeName === "party" || typeName === "approval");
558
+ const byName = partyParameter &&
559
+ (subjectPartyRoles.includes(param.key) ||
560
+ document.parties[param.key])
561
+ ? { kind: "name", value: param.key, span: origin }
562
+ : undefined;
563
+ const actual = supplied.get(param.key) ??
564
+ byName ??
565
+ (fallback && {
566
+ ...fallback,
567
+ source: declarationSources.get(decl) ?? "program",
568
+ });
216
569
  if (!actual) {
217
570
  if (type.kind === "type" && type.optional)
218
571
  continue;
219
- fail({ span: origin }, `${id} needs ${param.key}`, `add ${param.key}: value inside ${id}`);
572
+ failWithCode({ span: origin }, partyParameter ? "subject_party_unbound" : "HSX1001", `${id} needs ${param.key}`, `add ${param.key}: value inside ${id}`);
220
573
  }
221
574
  environment.set(param.key, actual);
222
575
  }
223
576
  for (const key of supplied.keys())
224
577
  if (!decl.parameters.some((p) => p.key === key))
225
578
  fail(supplied.get(key), `unknown tunable ${key}`, `choose ${decl.parameters.map((p) => p.key).join(", ")}`);
226
- const resolve = (expr, seen = new Set()) => {
579
+ const isParty = (name) => !!document.parties[name] ||
580
+ (!!attachmentInfo &&
581
+ subjectPartyRoles.includes(name));
582
+ const resolvedParties = new Set();
583
+ const resolve = (expr, seen = new Set(), partyBinding = false) => {
227
584
  if (expr.kind === "call" &&
228
585
  ["object", "all", "party"].includes(expr.name)) {
229
586
  if (expr.args.length !== 1)
@@ -231,15 +588,19 @@ export function compile(source, options = {}) {
231
588
  const type = text(expr.args[0]);
232
589
  const matches = expr.name === "party"
233
590
  ? Object.entries(document.parties)
234
- .filter(([, party]) => party.kind === type)
591
+ .filter(([name, party]) => party.kind === type && (!partyBinding || names.has(name)))
235
592
  .map(([name]) => name)
236
- : [...objects.values()]
237
- .filter((object) => object.object === type ||
238
- type.startsWith(`${object.object}.`))
239
- .map((object) => object.name +
240
- type.slice(object.object.length).replaceAll(".", "_"));
593
+ : [...assignments.values()]
594
+ .filter((assignment) => expr.name === "all" ||
595
+ !attachmentSubjects.has(assignment.name) ||
596
+ attachmentSubjects.get(assignment.name) ===
597
+ attachmentInfo?.subjectKindId)
598
+ .filter((assignment) => assignment.target === type ||
599
+ type.startsWith(`${assignment.target}.`))
600
+ .map((assignment) => assignment.name +
601
+ type.slice(assignment.target.length).replaceAll(".", "_"));
241
602
  if (expr.name !== "all" && matches.length !== 1)
242
- fail({ span: origin }, `${id} needs ${expr.name === "all" ? "at least one" : "exactly one"} ${type}`, "declare the required object or supply this tunable explicitly");
603
+ failWithCode(expr, partyBinding ? "subject_party_unbound" : "HSX1001", `${id} needs ${expr.name === "all" ? "at least one" : "exactly one"} ${type}`, "declare the required object or supply this tunable explicitly");
243
604
  const items = matches.map((value) => ({
244
605
  kind: "name",
245
606
  value,
@@ -251,27 +612,45 @@ export function compile(source, options = {}) {
251
612
  }
252
613
  if (expr.kind !== "name")
253
614
  return expr;
615
+ const binding = environment.get(expr.value);
616
+ if (attachmentInfo &&
617
+ subjectPartyRoles.includes(expr.value) &&
618
+ (!binding ||
619
+ (binding.kind === "name" && binding.value === expr.value)))
620
+ return expr;
621
+ if (attachmentInfo) {
622
+ const [local, ...tail] = expr.value.split(".");
623
+ const target = `${attachmentInfo.subjectKindId}_${local}`;
624
+ if (attachmentSubjects.has(target))
625
+ return { ...expr, value: [target, ...tail].join("_") };
626
+ }
254
627
  if (expr.value.startsWith("party.")) {
255
- const binding = environment.get(expr.value.slice(6));
256
- if (binding?.kind === "name" && document.parties[binding.value])
257
- return { ...expr, value: `party.${binding.value}` };
628
+ const [, party, ...members] = expr.value.split(".");
629
+ const binding = environment.get(party);
630
+ if (binding?.kind === "name" && isParty(binding.value))
631
+ return {
632
+ ...expr,
633
+ value: ["party", binding.value, ...members].join("."),
634
+ };
258
635
  }
259
636
  const [root, ...tail] = expr.value.split(".");
260
637
  const bound = environment.get(root);
261
638
  if (!bound || (bound.kind === "name" && bound.value === root))
262
639
  return expr;
263
640
  if (seen.has(root))
264
- return fail(expr, `cyclic tunable ${root}`, "replace the cycle with a literal or declared reference");
265
- let resolved = supplied.has(root) || inherited.has(root) || enums.has(root)
641
+ return failWithCode(expr, partyBinding ? "subject_party_unbound" : "HSX1001", `cyclic tunable ${root}`, "replace the cycle with a literal or declared reference");
642
+ let resolved = resolvedParties.has(root) ||
643
+ (!partyBinding &&
644
+ (supplied.has(root) || inherited.has(root) || enums.has(root)))
266
645
  ? bound
267
- : resolve(bound, new Set([...seen, root]));
646
+ : resolve(bound, new Set([...seen, root]), partyBinding);
268
647
  for (const key of tail) {
269
648
  if (resolved.kind !== "block")
270
649
  return expr;
271
650
  const child = entries(resolved).get(key);
272
651
  if (!child)
273
652
  return fail(expr, `missing tunable ${expr.value}`, `declare ${key} in ${root}`);
274
- resolved = resolve(child, new Set([...seen, root]));
653
+ resolved = resolve(child, new Set([...seen, root]), partyBinding);
275
654
  }
276
655
  return resolved;
277
656
  };
@@ -281,7 +660,9 @@ export function compile(source, options = {}) {
281
660
  continue;
282
661
  const t = param.value.kind === "default" ? param.value.type : param.value;
283
662
  const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
284
- const v = supplied.has(param.key) || type === "enum" ? actual : resolve(actual);
663
+ const v = (supplied.has(param.key) && !attachmentInfo) || type === "enum"
664
+ ? actual
665
+ : resolve(actual, new Set(), !!attachmentInfo && (type === "party" || type === "approval"));
285
666
  environment.set(param.key, v);
286
667
  if (type === "enum" && t.kind === "call") {
287
668
  if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
@@ -292,8 +673,21 @@ export function compile(source, options = {}) {
292
673
  fail(v, `${param.key} needs a list`, "write [value, value]");
293
674
  }
294
675
  else if (type === "party" || type === "approval") {
295
- if (v.kind !== "name" || !document.parties[v.value])
296
- fail(v, `${param.key} needs a declared party`, "declare a party and use its name here");
676
+ if (v.kind !== "name" || !isParty(v.value))
677
+ failWithCode(actual, attachmentInfo ? "subject_party_unbound" : "HSX1001", `${param.key} needs a declared party`, "declare a party and use its name here");
678
+ const party = document.parties[v.value];
679
+ if ((type === "approval" && (party?.kind !== "staff" || !party.role)) ||
680
+ (type === "party" &&
681
+ (party?.kind === "staff" ||
682
+ (attachmentInfo && party?.kind === "person"))))
683
+ failWithCode(actual, "party_kind_mismatch", `${param.key} cannot bind ${v.value}`, type === "approval"
684
+ ? "use a declared staff party with a role"
685
+ : "use a subject role or declared business");
686
+ resolvedParties.add(param.key);
687
+ if (attachmentInfo)
688
+ attachmentInfo.parties[param.key] = subjectPartyRoles.includes(v.value)
689
+ ? { role: v.value }
690
+ : { party: v.value };
297
691
  if (type === "approval")
298
692
  approvers.add(text(v));
299
693
  }
@@ -309,11 +703,14 @@ export function compile(source, options = {}) {
309
703
  fail(value, "reference needs an object name", "name a declared object");
310
704
  const [root, ...tail] = value.value.split(".");
311
705
  const obj = objects.get(root);
706
+ const assignment = assignments.get(root);
707
+ const targetType = obj ? obj.name : assignment?.target;
312
708
  if ((!obj &&
709
+ !assignment &&
313
710
  !document.instruments.some((inst) => inst.id === value.value)) ||
314
711
  (t.kind === "type" &&
315
712
  t.target &&
316
- [obj?.object, ...tail].join(".") !== t.target))
713
+ [targetType, ...tail].join(".") !== t.target))
317
714
  fail(value, `${param.key} has the wrong object type`, `use an object of type ${t.kind === "type" ? t.target : "ref"}`);
318
715
  if (seen.has(value.value))
319
716
  fail(value, "duplicate reference", "list each object once");
@@ -373,15 +770,30 @@ export function compile(source, options = {}) {
373
770
  }
374
771
  for (const key of body.keys())
375
772
  if (![
773
+ "familyRevision",
376
774
  "fields",
377
775
  "lifecycle",
378
776
  "records",
379
777
  "summary",
380
778
  "invariants",
381
779
  "constraints",
780
+ "reports",
781
+ "revisioned",
382
782
  ].includes(key) &&
383
783
  !key.startsWith("action "))
384
784
  fail(decl, `unknown instrument clause ${key}`, "use fields, lifecycle, actions, invariants, or records");
785
+ if (body.has("familyRevision")) {
786
+ const revExpr = body.get("familyRevision");
787
+ const revVal = literal(revExpr);
788
+ if (typeof revVal !== "number" ||
789
+ !Number.isInteger(revVal) ||
790
+ revVal <= 0) {
791
+ fail(revExpr, "familyRevision must be a positive integer", "use a positive integer revision");
792
+ }
793
+ if (familyDeclaration) {
794
+ familyDeclaration.revision = revVal;
795
+ }
796
+ }
385
797
  const records = entries(asBlock(body.get("records")));
386
798
  if (!inherited.size)
387
799
  environment.set("parent", { kind: "name", value: id, span: origin });
@@ -394,12 +806,54 @@ export function compile(source, options = {}) {
394
806
  });
395
807
  const fields = [];
396
808
  const calculations = [];
809
+ let currentAction;
810
+ let currentActionName;
397
811
  const path = (expr) => {
398
812
  const value = resolve(expr);
399
813
  const name = text(value);
400
- if (document.parties[name])
814
+ if (isParty(name))
401
815
  return `party.${name}`;
402
- return /^(self|input|party)\./.test(name) ? name : `self.${name}`;
816
+ if (name.startsWith("subject.")) {
817
+ const subField = name.split(".")[1];
818
+ if (currentAction) {
819
+ const req = currentAction.subject?.requirements.find((r) => r.field.name === subField);
820
+ if (!req) {
821
+ failWithCode(expr, "subject_field_unknown", `subject.${subField} names no declared subject requirement in action ${currentActionName}`, `declare ${subField} in subject { ... }`);
822
+ }
823
+ }
824
+ else {
825
+ const declaredInAction = decl.body.entries.some((e) => {
826
+ if (!e.key.startsWith("action "))
827
+ return false;
828
+ const subBlock = entries(asBlock(e.value)).get("subject");
829
+ if (!subBlock)
830
+ return false;
831
+ return asBlock(subBlock).entries.some((se) => {
832
+ if (se.key === subField)
833
+ return true;
834
+ if (se.key === "adapter") {
835
+ const names = se.value.kind === "list"
836
+ ? se.value.items.map(text)
837
+ : [text(se.value)];
838
+ return names.some((n) => {
839
+ const reg = options.adapterRegistry?.[n];
840
+ if (!reg)
841
+ return false;
842
+ const op = reg.adapter.operationMap[reg.operation];
843
+ return op?.subjectRequirements?.some((sr) => sr.name === subField);
844
+ });
845
+ }
846
+ return false;
847
+ });
848
+ });
849
+ if (!declaredInAction) {
850
+ failWithCode(expr, "subject_field_unknown", `subject.${subField} names no declared subject requirement in instrument ${id}`, `declare ${subField} in an action subject { ... }`);
851
+ }
852
+ }
853
+ }
854
+ return /^(self|input|party|subject)\./.test(name)
855
+ ? name
856
+ : `self.${name}`;
403
857
  };
404
858
  const val = (expr) => {
405
859
  const v = resolve(expr);
@@ -414,7 +868,44 @@ export function compile(source, options = {}) {
414
868
  return id;
415
869
  const value = resolve(expr);
416
870
  if (value.kind === "block") {
417
- const result = Object.fromEntries([...entries(value)].map(([key, value]) => [key, data(value)]));
871
+ const rawEntries = entries(value);
872
+ if ((rawEntries.has("family") && !rawEntries.has("kind")) ||
873
+ ((rawEntries.has("states") ||
874
+ rawEntries.has("reference") ||
875
+ rawEntries.has("anchor")) &&
876
+ rawEntries.has("instrument"))) {
877
+ let famTuple;
878
+ if (rawEntries.has("family")) {
879
+ const famExpr = rawEntries.get("family");
880
+ const famStr = famExpr.kind === "name" ? famExpr.value : text(famExpr);
881
+ famTuple = resolveFamily(famStr, famExpr);
882
+ }
883
+ let instrumentVal;
884
+ if (rawEntries.has("instrument")) {
885
+ instrumentVal = data(rawEntries.get("instrument"));
886
+ if (famTuple) {
887
+ checkTargetFamily(instrumentVal, famTuple, rawEntries.get("instrument"));
888
+ }
889
+ }
890
+ else if (famTuple) {
891
+ const matched = resolveFamilyInstruments(famTuple, id, rawEntries.get("family"));
892
+ instrumentVal = matched.length === 1 ? matched[0] : matched;
893
+ }
894
+ const result = {};
895
+ if (famTuple) {
896
+ result.family = famTuple;
897
+ }
898
+ if (instrumentVal !== undefined) {
899
+ result.instrument = instrumentVal;
900
+ }
901
+ for (const [k, v] of rawEntries) {
902
+ if (k === "family" || k === "instrument")
903
+ continue;
904
+ result[k] = data(v);
905
+ }
906
+ return result;
907
+ }
908
+ const result = Object.fromEntries([...rawEntries].map(([key, value]) => [key, data(value)]));
418
909
  const selection = result.selection;
419
910
  if (Array.isArray(selection?.instrument) &&
420
911
  selection.instrument.length === 0) {
@@ -457,48 +948,77 @@ export function compile(source, options = {}) {
457
948
  for (const row of block.entries) {
458
949
  const t = row.value.kind === "default" ? row.value.type : row.value;
459
950
  const constant = row.value.kind === "default" ? resolve(row.value.value) : undefined;
460
- const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
461
- const f = {
462
- name: row.key,
463
- type,
464
- ...(t.kind === "type" && t.optional ? { optional: true } : {}),
465
- };
466
- if (type === "enum" && t.kind === "call")
467
- f.values = t.args.map(text);
468
- if (["integer", "money"].includes(type) && t.kind === "call") {
469
- if (t.args.length !== 2)
470
- fail(t, "bounded fields need a minimum and maximum", "write integer(1, 12) or money(0 SAR, 100 SAR)");
471
- f.minimum = literal(resolve(t.args[0]));
472
- f.maximum = literal(resolve(t.args[1]));
473
- }
951
+ const f = lowerFieldShape(row, resolve);
952
+ const type = f.type;
474
953
  if (type === "list" && t.kind === "call") {
475
- f.item = text(t.args[0]);
476
- f.maxItems = t.args[1] ? literal(resolve(t.args[1])) : 366;
477
- }
478
- if (type === "list" && t.kind === "type") {
479
- f.item = t.target;
480
- f.maxItems = 366;
954
+ const item = t.args[0];
955
+ if (item.kind === "type" && item.name === "ref" && item.target) {
956
+ f.target = text(resolve({ kind: "name", value: item.target, span: item.span })).replaceAll(".", "_");
957
+ f.targetKind = objects.has(String(f.target))
958
+ ? "object"
959
+ : "instrument";
960
+ }
481
961
  }
482
962
  if (type === "ref") {
483
- const target = t.kind === "type" ? t.target : undefined;
484
- if (!target)
485
- fail(row, "reference needs a target", "write ref<object>");
486
- const [root, ...tail] = target.split(".");
487
- const resolved = environment.get(root);
488
- const resolvedTargets = resolved?.kind === "list"
489
- ? resolved.items
490
- : resolved
491
- ? [resolved]
492
- : [];
493
- const targets = resolvedTargets.map((value) => [text(resolve(value)).replaceAll(".", "_"), ...tail].join("_"));
494
- f.target =
495
- target === "self"
496
- ? id
497
- : targets.length === 1
498
- ? targets[0]
499
- : targets.length
500
- ? targets
501
- : target;
963
+ if (t.kind === "block") {
964
+ const b = entries(t);
965
+ const famNode = b.get("targetFamily") ?? b.get("family");
966
+ let targetFamTuple;
967
+ if (famNode) {
968
+ const famStr = famNode.kind === "name" ? famNode.value : text(famNode);
969
+ targetFamTuple = resolveFamily(famStr, famNode);
970
+ f.targetFamily = targetFamTuple;
971
+ }
972
+ if (b.has("target") || b.has("instrument")) {
973
+ const tgtExpr = (b.get("target") ?? b.get("instrument"));
974
+ const resolvedTgt = resolve(tgtExpr);
975
+ let tgtVal;
976
+ if (resolvedTgt.kind === "list") {
977
+ const items = resolvedTgt.items.map((it) => text(resolve(it)).replaceAll(".", "_"));
978
+ tgtVal = items.length === 1 ? items[0] : items;
979
+ }
980
+ else {
981
+ tgtVal = text(resolvedTgt).replaceAll(".", "_");
982
+ }
983
+ f.target = tgtVal;
984
+ if (targetFamTuple) {
985
+ checkTargetFamily(tgtVal, targetFamTuple, tgtExpr);
986
+ }
987
+ }
988
+ else if (targetFamTuple) {
989
+ const matched = resolveFamilyInstruments(targetFamTuple, id, famNode);
990
+ f.target = matched.length === 1 ? matched[0] : matched;
991
+ }
992
+ f.targetKind =
993
+ typeof f.target === "string" && objects.has(f.target)
994
+ ? "object"
995
+ : "instrument";
996
+ }
997
+ else {
998
+ const target = t.kind === "type" ? t.target : undefined;
999
+ if (!target)
1000
+ fail(row, "reference needs a target", "write ref<object>");
1001
+ const [root, ...tail] = target.split(".");
1002
+ const resolved = environment.get(root);
1003
+ const resolvedTargets = resolved?.kind === "list"
1004
+ ? resolved.items
1005
+ : resolved
1006
+ ? [resolved]
1007
+ : [];
1008
+ const targets = resolvedTargets.map((value) => [text(resolve(value)).replaceAll(".", "_"), ...tail].join("_"));
1009
+ f.target =
1010
+ target === "self"
1011
+ ? id
1012
+ : targets.length === 1
1013
+ ? targets[0]
1014
+ : targets.length
1015
+ ? targets
1016
+ : target;
1017
+ f.targetKind =
1018
+ typeof f.target === "string" && objects.has(f.target)
1019
+ ? "object"
1020
+ : "instrument";
1021
+ }
502
1022
  }
503
1023
  else if (type === "account") {
504
1024
  if (t.kind === "call") {
@@ -508,8 +1028,10 @@ export function compile(source, options = {}) {
508
1028
  f.book = t.args[1] ? text(t.args[1]) : "cash";
509
1029
  if (t.args[2]) {
510
1030
  const mode = text(t.args[2]);
511
- if (["contra", "external"].includes(mode))
512
- f[mode] = true;
1031
+ if (mode === "external")
1032
+ fail(t, "external account mode was removed", "use a reservation and instruction-bound evidence");
1033
+ if (mode === "contra")
1034
+ f.contra = true;
513
1035
  else
514
1036
  f.key = mode;
515
1037
  }
@@ -620,6 +1142,7 @@ export function compile(source, options = {}) {
620
1142
  lifecycle.transitions = {};
621
1143
  const inst = {
622
1144
  id,
1145
+ ...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
623
1146
  title: title(id),
624
1147
  summary: body.has("summary")
625
1148
  ? String(data(body.get("summary")))
@@ -637,21 +1160,213 @@ export function compile(source, options = {}) {
637
1160
  entries: block.entries.flatMap((entry) => {
638
1161
  if (!entry.key.startsWith("when "))
639
1162
  return [entry];
640
- const [, tunable, , choice] = entry.key.split(" ");
1163
+ const [, tunable, relation, choice] = entry.key.split(" ");
641
1164
  const binding = environment.get(tunable);
642
1165
  if (!binding)
643
- fail(entry, `unknown branch tunable ${tunable}`, "name an enum tunable declared by this header");
644
- if (!enums.get(tunable)?.includes(choice))
645
- fail(entry, `unknown enum branch ${choice}`, "use a declared enum value");
1166
+ fail(entry, `unknown branch tunable ${tunable}`, "name an enum or reference tunable declared by this header");
1167
+ let matches;
1168
+ if (relation === "has") {
1169
+ // Inspect declarations, not lowering order: a bound object may
1170
+ // appear after the instrument that asks about its fields.
1171
+ const bound = resolve(binding);
1172
+ const [root, ...children] = text(bound).split(".");
1173
+ const assignment = assignments.get(root);
1174
+ let target = assignment
1175
+ ? templates.get(assignment.target)?.body
1176
+ : program.decls
1177
+ .filter((decl) => decl.kind === "instrument")
1178
+ .find((decl) => decl.name === root)?.body;
1179
+ for (const child of children) {
1180
+ const record = target
1181
+ ? entries(asBlock(entries(target).get("records"))).get(child)
1182
+ : undefined;
1183
+ target = record ? asBlock(record) : undefined;
1184
+ }
1185
+ if (!target)
1186
+ fail(entry, "field branch needs a declared object", "bind a reference to a declared object");
1187
+ matches = entries(asBlock(entries(target).get("fields"))).has(choice);
1188
+ }
1189
+ else {
1190
+ if (!enums.get(tunable)?.includes(choice))
1191
+ fail(entry, `unknown enum branch ${choice}`, "use a declared enum value");
1192
+ matches = text(binding) === choice;
1193
+ }
646
1194
  const body = asBlock(entry.value);
647
1195
  for (const clause of body.entries)
648
1196
  if (!["requires", "moves", "invoke", "calculate"].includes(clause.key) &&
649
1197
  !clause.key.startsWith("when "))
650
1198
  fail(clause, "when permits requirements, calculations, moves and invocations", "keep lifecycle and actor clauses outside the branch");
651
- return text(binding) === choice ? selected(body).entries : [];
1199
+ return matches ? selected(body).entries : [];
652
1200
  }),
653
1201
  });
654
1202
  const slots = entries(selected(asBlock(row.value)));
1203
+ let actionSubject;
1204
+ let subjectExpr = slots.get("subject");
1205
+ const boundaryBindings = new Set();
1206
+ const authoredMoves = slots.get("moves");
1207
+ for (const move of authoredMoves?.kind === "list"
1208
+ ? authoredMoves.items
1209
+ : authoredMoves
1210
+ ? [authoredMoves]
1211
+ : []) {
1212
+ const parts = entries(asBlock(move));
1213
+ const boundary = parts.get("boundary");
1214
+ if (!boundary)
1215
+ continue;
1216
+ if ((parts.has("operation")
1217
+ ? String(data(parts.get("operation")))
1218
+ : "internal_transfer.create") !== "internal_transfer.reserve" ||
1219
+ parts.has("shares") ||
1220
+ parts.has("fee"))
1221
+ fail(move, "boundary dispatch requires a reservation", "reserve the exact amount before dispatch");
1222
+ const adapterExpr = entries(asBlock(boundary)).get("adapter");
1223
+ if (!adapterExpr)
1224
+ fail(boundary, "boundary needs an adapter", "name a bound ADL adapter");
1225
+ const binding = text(resolve(adapterExpr));
1226
+ const target = options.adapterRegistry &&
1227
+ Object.hasOwn(options.adapterRegistry, binding)
1228
+ ? options.adapterRegistry[binding]
1229
+ : undefined;
1230
+ if (!target ||
1231
+ !Object.hasOwn(target.adapter.operationMap, target.operation))
1232
+ fail(boundary, `unknown boundary adapter ${binding}`, "bind the named ADL adapter before compilation");
1233
+ boundaryBindings.add(binding);
1234
+ }
1235
+ if (boundaryBindings.size) {
1236
+ const block = asBlock(subjectExpr);
1237
+ const declared = new Set(block.entries
1238
+ .filter((entry) => entry.key === "adapter")
1239
+ .flatMap((entry) => entry.value.kind === "list"
1240
+ ? entry.value.items.map(text)
1241
+ : [text(entry.value)]));
1242
+ subjectExpr = {
1243
+ ...block,
1244
+ entries: [
1245
+ ...block.entries,
1246
+ ...[...boundaryBindings]
1247
+ .filter((binding) => !declared.has(binding))
1248
+ .map((binding) => ({
1249
+ key: "adapter",
1250
+ value: {
1251
+ kind: "name",
1252
+ value: binding,
1253
+ span: row.span,
1254
+ },
1255
+ span: row.span,
1256
+ })),
1257
+ ],
1258
+ };
1259
+ }
1260
+ if (subjectExpr) {
1261
+ const subjectBlock = asBlock(subjectExpr);
1262
+ const directRequirements = [];
1263
+ const adapterList = [];
1264
+ for (const entry of subjectBlock.entries) {
1265
+ if (entry.key === "adapter") {
1266
+ const bindingNames = entry.value.kind === "list"
1267
+ ? entry.value.items.map(text)
1268
+ : [text(entry.value)];
1269
+ for (const bindingName of bindingNames) {
1270
+ const target = options.adapterRegistry?.[bindingName];
1271
+ if (target) {
1272
+ const { adapter, operation } = target;
1273
+ const opBinding = adapter.operationMap[operation];
1274
+ if (opBinding &&
1275
+ opBinding.subjectRequirements !== undefined) {
1276
+ const validatedRequirements = [];
1277
+ for (const req of opBinding.subjectRequirements) {
1278
+ const result = udlObjectFieldSchema.safeParse(req);
1279
+ if (!result.success || result.data.optional) {
1280
+ fail(entry, `adapter requirement ${req.name} is invalid or optional: ${result.success ? "requirements cannot be optional" : result.error.message}`, "ensure adapter subject requirements conform to UDL schema");
1281
+ }
1282
+ validatedRequirements.push(result.data);
1283
+ }
1284
+ const declaration = {
1285
+ provider: adapter.provider,
1286
+ capability: adapter.capability,
1287
+ operation,
1288
+ requirements: validatedRequirements,
1289
+ };
1290
+ const digest = sha256(new TextEncoder().encode(canonicalJson(declaration)));
1291
+ const snapshot = {
1292
+ ...declaration,
1293
+ declarationDigest: Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join(""),
1294
+ };
1295
+ const adapterRenames = {};
1296
+ if (attachmentInfo?.renames) {
1297
+ for (const req of snapshot.requirements) {
1298
+ if (attachmentInfo.renames.has(req.name)) {
1299
+ adapterRenames[req.name] = attachmentInfo.renames.get(req.name);
1300
+ }
1301
+ }
1302
+ }
1303
+ adapterList.push({
1304
+ binding: bindingName,
1305
+ snapshot,
1306
+ ...(Object.keys(adapterRenames).length > 0
1307
+ ? { renames: adapterRenames }
1308
+ : {}),
1309
+ });
1310
+ for (const reqField of snapshot.requirements) {
1311
+ const objectField = attachmentInfo?.renames.get(reqField.name);
1312
+ const targetName = objectField ?? reqField.name;
1313
+ const existing = directRequirements.find((r) => (r.objectField ?? r.field.name) === targetName);
1314
+ if (existing) {
1315
+ if (!sameObjectField(existing.field, reqField)) {
1316
+ failWithCode(entry, "subject_field_conflict", `conflicting requirement ${reqField.name} in action ${name}`, "rename or unify the requirement");
1317
+ }
1318
+ }
1319
+ else {
1320
+ directRequirements.push({
1321
+ field: { ...reqField },
1322
+ ...(objectField ? { objectField } : {}),
1323
+ });
1324
+ }
1325
+ }
1326
+ }
1327
+ else {
1328
+ adapterList.push({
1329
+ binding: bindingName,
1330
+ snapshot: null,
1331
+ });
1332
+ }
1333
+ }
1334
+ else {
1335
+ adapterList.push({
1336
+ binding: bindingName,
1337
+ snapshot: null,
1338
+ });
1339
+ }
1340
+ }
1341
+ }
1342
+ else {
1343
+ const fieldDef = lowerObjectField(entry, resolve);
1344
+ if (fieldDef.optional) {
1345
+ fail(entry, `subject requirement ${entry.key} cannot be optional`, "remove ? from requirement");
1346
+ }
1347
+ const objectField = attachmentInfo?.renames.get(entry.key);
1348
+ const targetName = objectField ?? entry.key;
1349
+ const existing = directRequirements.find((r) => (r.objectField ?? r.field.name) === targetName);
1350
+ if (existing) {
1351
+ if (!sameObjectField(existing.field, fieldDef)) {
1352
+ failWithCode(entry, "subject_field_conflict", `conflicting requirement ${entry.key} in action ${name}`, "rename or unify the requirement");
1353
+ }
1354
+ }
1355
+ else {
1356
+ directRequirements.push({
1357
+ field: fieldDef,
1358
+ ...(objectField ? { objectField } : {}),
1359
+ });
1360
+ }
1361
+ }
1362
+ }
1363
+ if (directRequirements.length > 0 || adapterList.length > 0) {
1364
+ actionSubject = {
1365
+ requirements: directRequirements,
1366
+ adapters: adapterList,
1367
+ };
1368
+ }
1369
+ }
655
1370
  const a = {
656
1371
  summary: slots.has("summary")
657
1372
  ? String(data(slots.get("summary")))
@@ -662,7 +1377,14 @@ export function compile(source, options = {}) {
662
1377
  input: lowerFields(asBlock(slots.get("input"))),
663
1378
  requires: [],
664
1379
  moves: [],
1380
+ ...(actionSubject ? { subject: actionSubject } : {}),
665
1381
  };
1382
+ for (const binding of boundaryBindings)
1383
+ if (!a.subject?.adapters.find((entry) => entry.binding === binding)
1384
+ ?.snapshot)
1385
+ fail(row, `boundary adapter ${binding} has no declared requirements`, "declare the adapter subject requirements, including an explicit empty list");
1386
+ currentAction = a;
1387
+ currentActionName = name;
666
1388
  if (name !== "create") {
667
1389
  const from = slots.get("from");
668
1390
  const to = slots.get("to");
@@ -674,7 +1396,7 @@ export function compile(source, options = {}) {
674
1396
  };
675
1397
  }
676
1398
  for (const [key, expr] of slots) {
677
- if (["from", "to", "input", "summary"].includes(key))
1399
+ if (["from", "to", "input", "summary", "subject"].includes(key))
678
1400
  continue;
679
1401
  if (key === "moves") {
680
1402
  const moves = expr.kind === "list" ? expr.items : [expr];
@@ -695,6 +1417,17 @@ export function compile(source, options = {}) {
695
1417
  if (op === "internal_transfer.create" ||
696
1418
  op === "internal_transfer.reserve") {
697
1419
  const amount = parts.get("amount"), from = parts.get("from"), to = parts.get("to");
1420
+ if (amount) {
1421
+ const resolvedAmount = resolve(amount);
1422
+ if (resolvedAmount.kind === "name" &&
1423
+ resolvedAmount.value.startsWith("subject.")) {
1424
+ const subField = resolvedAmount.value.split(".")[1];
1425
+ const req = a.subject?.requirements.find((r) => r.field.name === subField);
1426
+ if (req && req.field.type !== "money") {
1427
+ fail(amount, `move amount subject.${subField} must have type money`, "use a money field");
1428
+ }
1429
+ }
1430
+ }
698
1431
  if (parts.has("shares")) {
699
1432
  if (!amount ||
700
1433
  !from ||
@@ -715,7 +1448,7 @@ export function compile(source, options = {}) {
715
1448
  span: rate.span,
716
1449
  });
717
1450
  if (recipient.kind !== "name" ||
718
- !document.parties[recipient.value] ||
1451
+ !isParty(recipient.value) ||
719
1452
  rate.kind !== "percent")
720
1453
  fail(rate, "split needs party percentages", "write party_name: 70%");
721
1454
  total += Number(literal(rate));
@@ -770,6 +1503,13 @@ export function compile(source, options = {}) {
770
1503
  ...transfer,
771
1504
  operation: op,
772
1505
  capture: String(data(parts.get("capture"))),
1506
+ ...(parts.has("boundary")
1507
+ ? {
1508
+ boundary: {
1509
+ adapter: text(resolve(entries(asBlock(parts.get("boundary"))).get("adapter"))),
1510
+ },
1511
+ }
1512
+ : {}),
773
1513
  }
774
1514
  : { ...transfer, operation: op });
775
1515
  else {
@@ -888,8 +1628,40 @@ export function compile(source, options = {}) {
888
1628
  else
889
1629
  a[key] = data(expr);
890
1630
  }
1631
+ if (attachmentInfo) {
1632
+ if (attachmentInfo.exposed.has(name)) {
1633
+ a.publicAction = attachmentInfo.exposed.get(name);
1634
+ }
1635
+ else {
1636
+ delete a.publicAction;
1637
+ }
1638
+ }
891
1639
  if (automatic(a.actor))
892
1640
  delete a.publicAction;
1641
+ const checkSubjectPaths = (obj, span) => {
1642
+ if (typeof obj === "string") {
1643
+ if (obj.startsWith("subject.")) {
1644
+ const subField = obj.split(".")[1];
1645
+ const req = a.subject?.requirements.find((r) => r.field.name === subField);
1646
+ if (!req) {
1647
+ failWithCode({ span }, "subject_field_unknown", `subject.${subField} names no declared subject requirement in action ${name}`, `declare ${subField} in subject { ... }`);
1648
+ }
1649
+ }
1650
+ }
1651
+ else if (Array.isArray(obj)) {
1652
+ for (const item of obj)
1653
+ checkSubjectPaths(item, span);
1654
+ }
1655
+ else if (obj !== null && typeof obj === "object") {
1656
+ for (const val of Object.values(obj))
1657
+ checkSubjectPaths(val, span);
1658
+ }
1659
+ };
1660
+ checkSubjectPaths(a.requires, row.span);
1661
+ checkSubjectPaths(a.set, row.span);
1662
+ checkSubjectPaths(a.invoke, row.span);
1663
+ currentAction = undefined;
1664
+ currentActionName = undefined;
893
1665
  for (const requirement of a.requires ?? []) {
894
1666
  if (requirement.kind !== "approval" ||
895
1667
  !approvers.has(requirement.party))
@@ -897,25 +1669,45 @@ export function compile(source, options = {}) {
897
1669
  const action = requirement.action ?? name;
898
1670
  const key = `${id}_${action}_decision`;
899
1671
  const previous = implicitDecisions.get(key);
900
- if (previous && previous.party !== requirement.party)
901
- fail({ span: origin }, `action ${action} has multiple approval parties`, "use a separate decision action for each party");
1672
+ if (previous &&
1673
+ (previous.party !== requirement.party ||
1674
+ previous.protectedRequest !==
1675
+ (requirement.protectedRequest ?? "self")))
1676
+ fail({ span: origin }, `action ${action} has conflicting approval parties or protected requests`, "use a separate decision action for each party");
902
1677
  implicitDecisions.set(key, {
903
1678
  target: id,
904
1679
  action,
905
1680
  party: requirement.party,
1681
+ protectedRequest: requirement.protectedRequest ?? "self",
906
1682
  origin,
907
1683
  });
908
1684
  }
1685
+ for (const requirement of a.subject?.requirements ?? []) {
1686
+ const entry = asBlock(subjectExpr).entries.find((entry) => entry.key === requirement.field.name) ??
1687
+ asBlock(subjectExpr).entries.find((entry) => entry.key === "adapter");
1688
+ requirementOrigins.set(requirement, {
1689
+ source: declarationSources.get(decl) ?? "program",
1690
+ span: entry?.span ?? row.span,
1691
+ message: `${id}.${name}.subject.${requirement.field.name}`,
1692
+ });
1693
+ }
909
1694
  inst.actions[name] = a;
910
1695
  inst.actionOrder.push(name);
911
1696
  }
912
- for (const key of ["invariants"])
1697
+ for (const key of ["invariants", "reports", "revisioned"])
913
1698
  if (body.has(key))
914
1699
  inst[key] = data(body.get(key));
915
1700
  origins.push({
916
1701
  path: `$.instruments[${document.instruments.length}]`,
917
1702
  span: { ...origin, ...lineColAt(source, origin.start) },
918
1703
  });
1704
+ if (familyDeclaration && familyDeclaration.revision !== undefined) {
1705
+ inst.family = {
1706
+ module: familyDeclaration.module,
1707
+ exportPath: familyDeclaration.exportPath,
1708
+ revision: familyDeclaration.revision,
1709
+ };
1710
+ }
919
1711
  document.instruments.push(inst);
920
1712
  for (const [key, definition] of records) {
921
1713
  const child = {
@@ -925,11 +1717,169 @@ export function compile(source, options = {}) {
925
1717
  body: asBlock(definition),
926
1718
  span: origin,
927
1719
  };
1720
+ declarationSources.set(child, declarationSources.get(decl) ?? "program");
1721
+ const childFamily = familyDeclaration
1722
+ ? {
1723
+ module: familyDeclaration.module,
1724
+ exportPath: `${familyDeclaration.exportPath}.${key}`,
1725
+ ...(familyDeclaration.revision !== undefined
1726
+ ? { revision: familyDeclaration.revision }
1727
+ : {}),
1728
+ }
1729
+ : undefined;
928
1730
  addInstrument(child, `${id}_${key}`, emptyBlock, origin, new Map([
929
1731
  ...environment,
930
1732
  ["parent", { kind: "name", value: id, span: origin }],
931
- ]), approvers, enums);
1733
+ ]), approvers, enums, attachmentInfo
1734
+ ? { ...attachmentInfo, exposed: new Map() }
1735
+ : undefined, childFamily);
1736
+ }
1737
+ };
1738
+ const compileObject = (decl) => {
1739
+ const body = entries(decl.body);
1740
+ for (const key of body.keys()) {
1741
+ if (key !== "fields" &&
1742
+ key !== "columns" &&
1743
+ !key.startsWith("attach ")) {
1744
+ fail(decl, `unknown object clause ${key}`, "use fields, columns, or attach");
1745
+ }
1746
+ }
1747
+ // 1. Lower authored fields
1748
+ const authoredFields = [];
1749
+ const authoredNames = [];
1750
+ const fieldsBlock = asBlock(body.get("fields"));
1751
+ for (const row of fieldsBlock.entries) {
1752
+ if (RESERVED_OBJECT_NAMES.some((name) => name === row.key)) {
1753
+ fail(row, `${row.key} is a reserved object name`, "rename this field");
1754
+ }
1755
+ const fieldDef = lowerObjectField(row);
1756
+ authoredFields.push(fieldDef);
1757
+ authoredNames.push(row.key);
932
1758
  }
1759
+ // 2. Process attachments
1760
+ const attachments = [];
1761
+ for (const entry of decl.body.entries) {
1762
+ if (!entry.key.startsWith("attach "))
1763
+ continue;
1764
+ const match = /^attach\s+([A-Za-z0-9_]+)\s*=\s*(.+)$/.exec(entry.key);
1765
+ if (!match) {
1766
+ fail(entry, "invalid attach syntax", "write attach name = template { ... }");
1767
+ }
1768
+ const attachmentName = match[1];
1769
+ const targetTemplate = match[2];
1770
+ const template = templates.get(targetTemplate);
1771
+ if (!template) {
1772
+ fail(entry, `unknown instrument ${targetTemplate}`, `add use ${targetTemplate.split(".")[0]} and choose a declared instrument`);
1773
+ }
1774
+ const instId = `${decl.name}_${attachmentName}`;
1775
+ const attachmentBlock = asBlock(entry.value);
1776
+ const renames = new Map();
1777
+ const renameEntries = new Map();
1778
+ const exposed = new Map();
1779
+ const tunableEntries = [];
1780
+ for (const row of attachmentBlock.entries) {
1781
+ if (row.key === "rename") {
1782
+ for (const r of asBlock(row.value).entries) {
1783
+ renames.set(r.key, text(r.value));
1784
+ renameEntries.set(r.key, r);
1785
+ }
1786
+ }
1787
+ else if (row.key === "expose") {
1788
+ if (row.value.kind === "call") {
1789
+ const actionName = row.value.name;
1790
+ const publicName = text(row.value.args[0]);
1791
+ exposed.set(actionName, publicName);
1792
+ }
1793
+ }
1794
+ else {
1795
+ tunableEntries.push(row);
1796
+ }
1797
+ }
1798
+ const parties = {};
1799
+ attachments.push({ name: attachmentName, instrument: instId, parties });
1800
+ const tunableBlock = {
1801
+ kind: "block",
1802
+ entries: tunableEntries,
1803
+ span: entry.value.span,
1804
+ };
1805
+ const templateFamily = resolveFamily(targetTemplate, entry, false);
1806
+ addInstrument(template, instId, tunableBlock, entry.span, new Map(), new Set(), new Map(), {
1807
+ subjectKindId: decl.name,
1808
+ attachmentName,
1809
+ renames,
1810
+ exposed,
1811
+ parties,
1812
+ }, templateFamily ? { ...templateFamily } : undefined);
1813
+ const attachedInst = document.instruments.find((i) => i.id === instId);
1814
+ if (attachedInst?.actions.create) {
1815
+ const owned = new Set(attachedInst.calculate.map((node) => node.target));
1816
+ for (const action of Object.values(attachedInst.actions)) {
1817
+ for (const node of action.calculate ?? [])
1818
+ owned.add(node.target);
1819
+ for (const move of action.moves)
1820
+ if ("capture" in move && move.capture)
1821
+ owned.add(move.capture);
1822
+ }
1823
+ const create = attachedInst.actions.create;
1824
+ for (const field of attachedInst.fields) {
1825
+ if (field.type === "account" ||
1826
+ (field.type === "ref" && field.targetKind === "instrument") ||
1827
+ (field.type === "list" &&
1828
+ field.item === "ref" &&
1829
+ field.targetKind === "instrument") ||
1830
+ field.optional ||
1831
+ "value" in field ||
1832
+ owned.has(field.name))
1833
+ continue;
1834
+ create.subject ??= { requirements: [], adapters: [] };
1835
+ const existing = create.subject.requirements.find((item) => item.field.name === field.name);
1836
+ if (existing) {
1837
+ if (canonicalJson(existing.field) !== canonicalJson(field))
1838
+ failWithCode(entry, "subject_field_conflict", `${instId}.create.subject.${field.name} conflicts with its instrument field`, "use the instrument field's type and constraints");
1839
+ continue;
1840
+ }
1841
+ const objectField = renames.get(field.name);
1842
+ const requirement = {
1843
+ field,
1844
+ ...(objectField ? { objectField } : {}),
1845
+ };
1846
+ create.subject.requirements.push(requirement);
1847
+ requirementOrigins.set(requirement, {
1848
+ source: declarationSources.get(template) ?? "program",
1849
+ span: entry.span,
1850
+ message: `${instId}.create.fields.${field.name}`,
1851
+ });
1852
+ }
1853
+ }
1854
+ for (const [oldName] of renames) {
1855
+ const found = attachedInst &&
1856
+ Object.values(attachedInst.actions).some((action) => action.subject?.requirements.some((requirement) => requirement.field.name === oldName));
1857
+ if (!found) {
1858
+ const renameEntry = renameEntries.get(oldName) ?? entry;
1859
+ failWithCode(renameEntry, "subject_field_unknown", `rename source '${oldName}' is not a declared subject requirement of ${targetTemplate}`, "rename a declared subject requirement");
1860
+ }
1861
+ }
1862
+ }
1863
+ // 4. Validate columns
1864
+ const columnsExpr = body.get("columns");
1865
+ let columns = [];
1866
+ if (columnsExpr) {
1867
+ if (columnsExpr.kind !== "list") {
1868
+ fail(columnsExpr, "columns needs a list of field names", "write columns: [name, ...]");
1869
+ }
1870
+ columns = columnsExpr.items.map(text);
1871
+ if (columns.length > 8) {
1872
+ fail(columnsExpr, "at most 8 columns allowed", "choose up to 8 columns");
1873
+ }
1874
+ }
1875
+ document.objects.push({
1876
+ id: decl.name,
1877
+ title: decl.title,
1878
+ authoredFields: authoredNames,
1879
+ attachments,
1880
+ fields: authoredFields,
1881
+ columns,
1882
+ });
933
1883
  };
934
1884
  for (const decl of program.decls) {
935
1885
  if (decl.kind === "instrument") {
@@ -937,11 +1887,123 @@ export function compile(source, options = {}) {
937
1887
  fail(decl, "program records cannot declare tunables", "put reusable instruments in a header");
938
1888
  addInstrument(decl, decl.name, emptyBlock, decl.span);
939
1889
  }
940
- if (decl.kind === "object") {
941
- const template = templates.get(decl.object);
1890
+ if (decl.kind === "assignment") {
1891
+ const template = templates.get(decl.target);
942
1892
  if (!template)
943
- fail(decl, `unknown object ${decl.object}`, `add use ${decl.object.split(".")[0]} and choose a declared object`);
944
- addInstrument(template, decl.name, decl.body, decl.span);
1893
+ fail(decl, `unknown instrument ${decl.target}`, `add use ${decl.target.split(".")[0]} and choose a declared instrument`);
1894
+ const templateFamily = resolveFamily(decl.target, decl, false);
1895
+ addInstrument(template, decl.name, decl.body, decl.span, new Map(), new Set(), new Map(), undefined, templateFamily ? { ...templateFamily } : undefined);
1896
+ }
1897
+ if (decl.kind === "object") {
1898
+ compileObject(decl);
1899
+ }
1900
+ }
1901
+ // Propagate mandatory invoked action requirements
1902
+ let changedInvocations = true;
1903
+ let invocationIterations = 0;
1904
+ while (changedInvocations && invocationIterations < 32) {
1905
+ changedInvocations = false;
1906
+ invocationIterations++;
1907
+ for (const inst of document.instruments) {
1908
+ for (const action of Object.values(inst.actions)) {
1909
+ for (const call of action.invoke ?? []) {
1910
+ if (call.guard)
1911
+ continue;
1912
+ let targetIds = [];
1913
+ if ("instrument" in call) {
1914
+ targetIds = [call.instrument];
1915
+ }
1916
+ else if ("selection" in call) {
1917
+ targetIds = Array.isArray(call.selection.instrument)
1918
+ ? call.selection.instrument
1919
+ : [call.selection.instrument];
1920
+ }
1921
+ else if ("reference" in call) {
1922
+ const refField = resolveField(document, inst, call.reference, action.input, action);
1923
+ if (refField?.type === "ref" &&
1924
+ refField.targetKind === "instrument") {
1925
+ targetIds = Array.isArray(refField.target)
1926
+ ? refField.target
1927
+ : [refField.target];
1928
+ }
1929
+ }
1930
+ for (const targetId of targetIds) {
1931
+ const targetInst = document.instruments.find((i) => i.id === targetId);
1932
+ if (!targetInst)
1933
+ continue;
1934
+ if (inst.subject &&
1935
+ targetInst.subject &&
1936
+ inst.subject === targetInst.subject) {
1937
+ const targetAction = targetInst.actions[call.action];
1938
+ if (!targetAction?.subject)
1939
+ continue;
1940
+ if (!action.subject) {
1941
+ action.subject = { requirements: [], adapters: [] };
1942
+ }
1943
+ for (const adapter of targetAction.subject.adapters) {
1944
+ if (!action.subject.adapters.some((existing) => existing.binding === adapter.binding)) {
1945
+ action.subject.adapters.push(adapter);
1946
+ changedInvocations = true;
1947
+ }
1948
+ }
1949
+ for (const targetReq of targetAction.subject.requirements) {
1950
+ const targetObjFieldName = targetReq.objectField ?? targetReq.field.name;
1951
+ const existing = action.subject.requirements.find((cr) => (cr.objectField ?? cr.field.name) === targetObjFieldName);
1952
+ if (!existing) {
1953
+ const inherited = {
1954
+ field: { ...targetReq.field, name: targetObjFieldName },
1955
+ };
1956
+ action.subject.requirements.push(inherited);
1957
+ requirementOrigins.set(inherited, requirementOrigins.get(targetReq));
1958
+ changedInvocations = true;
1959
+ }
1960
+ else if (!sameObjectField(existing.field, targetReq.field)) {
1961
+ const first = requirementOrigins.get(existing);
1962
+ const second = requirementOrigins.get(targetReq);
1963
+ throw new CompileFailure({
1964
+ code: "subject_field_conflict",
1965
+ message: `${first.message} conflicts with ${second.message}: incompatible requirement '${targetObjFieldName}'`,
1966
+ fix: "ensure compatible requirement definitions across invoked actions",
1967
+ span: first.span,
1968
+ source: first.source,
1969
+ related: [first, second],
1970
+ });
1971
+ }
1972
+ }
1973
+ }
1974
+ }
1975
+ }
1976
+ }
1977
+ }
1978
+ }
1979
+ for (const kind of document.objects) {
1980
+ const origins = new Map(kind.fields.map((field) => [
1981
+ field.name,
1982
+ `authored field ${kind.id}.${field.name}`,
1983
+ ]));
1984
+ for (const instrument of document.instruments.filter((item) => item.subject === kind.id)) {
1985
+ for (const name of instrument.actionOrder) {
1986
+ for (const requirement of instrument.actions[name].subject
1987
+ ?.requirements ?? []) {
1988
+ const field = {
1989
+ ...requirement.field,
1990
+ name: requirement.objectField ?? requirement.field.name,
1991
+ };
1992
+ const previous = kind.fields.find((item) => item.name === field.name);
1993
+ const origin = `${instrument.id}.${name}.subject.${requirement.field.name}`;
1994
+ if (previous && !sameObjectField(previous, field)) {
1995
+ failWithCode(objects.get(kind.id), "subject_field_conflict", `${origin} conflicts with ${origins.get(field.name)}: incompatible types or constraints`, "rename fields with different meanings");
1996
+ }
1997
+ if (!previous) {
1998
+ kind.fields.push(field);
1999
+ origins.set(field.name, origin);
2000
+ }
2001
+ }
2002
+ }
2003
+ }
2004
+ for (const column of kind.columns) {
2005
+ if (!kind.fields.some((field) => field.name === column))
2006
+ failWithCode(objects.get(kind.id), "subject_field_unknown", `column ${column} is unknown on ${kind.id}`, "name an authored field or attached requirement");
945
2007
  }
946
2008
  }
947
2009
  const eliminatedStates = new Map();
@@ -974,7 +2036,8 @@ export function compile(source, options = {}) {
974
2036
  for (let n = 0; n < inst.lifecycle.states.length; n++)
975
2037
  for (const edge of Object.values(inst.lifecycle.transitions))
976
2038
  if (edge.from.some((state) => reachable.has(state)))
977
- reachable.add(edge.to);
2039
+ if (edge.to !== "preserve")
2040
+ reachable.add(edge.to);
978
2041
  for (const [key, edge] of Object.entries(inst.lifecycle.transitions)) {
979
2042
  edge.from = edge.from.filter((state) => reachable.has(state));
980
2043
  if (!edge.from.length) {
@@ -1013,13 +2076,36 @@ export function compile(source, options = {}) {
1013
2076
  continue;
1014
2077
  const existing = document.instruments.some((inst) => Object.values(inst.actions).some((action) => action.approval?.action === decision.action &&
1015
2078
  action.approval.party === decision.party &&
1016
- inst.fields.some((field) => field.type === "ref" &&
1017
- field.target === decision.target &&
1018
- `self.${field.name}` === action.approval?.target)));
2079
+ inst.fields.some((field) => {
2080
+ if (field.type !== "ref" ||
2081
+ field.target !== decision.target ||
2082
+ `self.${field.name}` !== action.approval?.target)
2083
+ return false;
2084
+ const [root, name, ...tail] = decision.protectedRequest.split(".");
2085
+ const input = name && action.approval.input[name];
2086
+ const request = root === "self"
2087
+ ? [action.approval.target, name, ...tail]
2088
+ .filter(Boolean)
2089
+ .join(".")
2090
+ : root === "input" && name
2091
+ ? input && "field" in input
2092
+ ? [input.field, ...tail].join(".")
2093
+ : materialApprovals.has(action)
2094
+ ? [`self.material_${name}`, ...tail].join(".")
2095
+ : undefined
2096
+ : undefined;
2097
+ return (request !== undefined &&
2098
+ action.approval.protectedRequest === request);
2099
+ })));
1019
2100
  if (existing)
1020
2101
  continue;
1021
2102
  if (document.instruments.some((inst) => inst.id === id))
1022
2103
  fail({ span: decision.origin }, `implicit decision name ${id} is already used`, "rename the conflicting object");
2104
+ const protectedRequest = decision.protectedRequest === "self"
2105
+ ? "self.target"
2106
+ : decision.protectedRequest.startsWith("self.")
2107
+ ? `self.target.${decision.protectedRequest.slice(5)}`
2108
+ : fail({ span: decision.origin }, "implicit approval needs a stored protected request", "declare an explicit decision for an input-based protected request");
1023
2109
  addInstrument(template, id, {
1024
2110
  kind: "block",
1025
2111
  span: decision.origin,
@@ -1027,16 +2113,27 @@ export function compile(source, options = {}) {
1027
2113
  for: decision.target,
1028
2114
  approved_by: decision.party,
1029
2115
  action: decision.action,
2116
+ protected_request: protectedRequest,
1030
2117
  }).map(([key, value]) => ({
1031
2118
  key,
1032
2119
  value: {
1033
- kind: key === "action" ? "text" : "name",
2120
+ kind: key === "action" || key === "protected_request"
2121
+ ? "text"
2122
+ : "name",
1034
2123
  value,
1035
2124
  span: decision.origin,
1036
2125
  },
1037
2126
  span: decision.origin,
1038
2127
  })),
1039
- }, decision.origin);
2128
+ }, decision.origin, new Map(), new Set(), new Map(), document.instruments.find((instrument) => instrument.id === decision.target)?.subject
2129
+ ? {
2130
+ subjectKindId: document.instruments.find((instrument) => instrument.id === decision.target).subject,
2131
+ attachmentName: id,
2132
+ parties: {},
2133
+ renames: new Map(),
2134
+ exposed: new Map(),
2135
+ }
2136
+ : undefined);
1040
2137
  }
1041
2138
  }
1042
2139
  // Material approval fields are inferred from the selected action's typed input.
@@ -1099,7 +2196,7 @@ export function compile(source, options = {}) {
1099
2196
  .reverse()
1100
2197
  .find((o) => i.path.startsWith(o.path));
1101
2198
  return diagnostic({
1102
- code: "HSX1601",
2199
+ code: i.code.startsWith("UDL") ? "HSX1601" : i.code,
1103
2200
  message: `${i.path}: ${i.message}`,
1104
2201
  fix: i.fix,
1105
2202
  span: origin?.span ?? program.span,