@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
package/src/compile.ts CHANGED
@@ -1,19 +1,35 @@
1
+ import { hash as sha256 } from "fast-sha256";
1
2
  import { buildUdlCostManifest, type UdlCostManifest } from "./cost.ts";
2
3
  import {
3
4
  validateUdl,
5
+ resolveField,
6
+ sameObjectField,
7
+ subjectPartyRoles,
8
+ RESERVED_OBJECT_NAMES,
9
+ type UdlObjectAttachment,
10
+ type AttachmentPartyBinding,
11
+ type SubjectPartyRole,
12
+ udlObjectFieldSchema,
4
13
  type UdlAction,
14
+ type UdlActionSubject,
15
+ type UdlAdapterSubjectSnapshot,
5
16
  type UdlCalculation,
6
17
  type UdlDocument,
18
+ type UdlFamily,
7
19
  type UdlField,
8
20
  type UdlInstrument,
21
+ type UdlObjectField,
22
+ type UdlSubjectRequirement,
9
23
  type UdlValue,
10
24
  } from "@hyperscale0/udl";
11
25
  import { tunableBounds } from "./tunables.ts";
12
26
  import { parseProgram } from "./parse.ts";
13
27
  import {
14
28
  lineColAt,
29
+ type AssignmentDecl,
15
30
  type BlockExpr,
16
31
  type Diagnostic,
32
+ type Entry,
17
33
  type Expr,
18
34
  type InstrumentDecl,
19
35
  type ObjectDecl,
@@ -21,6 +37,8 @@ import {
21
37
  } from "./ast.ts";
22
38
  import { bundledStandardLibrary, type StandardLibrary } from "./std-library.ts";
23
39
 
40
+ import type { ProviderAdapter } from "@hyperscale0/adl";
41
+
24
42
  export interface CompileDiagnostic extends Diagnostic {
25
43
  line: number;
26
44
  column: number;
@@ -31,8 +49,13 @@ export interface CompileOriginMapEntry {
31
49
  path: string;
32
50
  span: Span & { line: number; column: number };
33
51
  }
52
+ export interface AdapterBindingTarget {
53
+ adapter: ProviderAdapter;
54
+ operation: string;
55
+ }
34
56
  export interface CompileOptions {
35
57
  standardLibrary?: StandardLibrary;
58
+ adapterRegistry?: Readonly<Record<string, AdapterBindingTarget>>;
36
59
  }
37
60
  export interface CompileResult {
38
61
  verdict: "valid" | "invalid";
@@ -48,9 +71,154 @@ class CompileFailure extends Error {
48
71
  super(diagnostic.message);
49
72
  }
50
73
  }
51
- function fail(expr: { span: Span }, message: string, fix: string): never {
52
- throw new CompileFailure({ code: "HSX1001", message, fix, span: expr.span });
74
+ function fail(
75
+ expr: { span: Span; source?: string },
76
+ message: string,
77
+ fix: string,
78
+ ): never {
79
+ return failWithCode(expr, "HSX1001", message, fix);
80
+ }
81
+ function failWithCode(
82
+ expr: { span: Span; source?: string },
83
+ code: string,
84
+ message: string,
85
+ fix: string,
86
+ ): never {
87
+ throw new CompileFailure({
88
+ code,
89
+ message,
90
+ fix,
91
+ span: expr.span,
92
+ ...(expr.source ? { source: expr.source } : {}),
93
+ });
53
94
  }
95
+ function lowerFieldShape(
96
+ row: Entry,
97
+ resolveExpr: (e: Expr) => Expr = (e) => e,
98
+ ): Record<string, unknown> {
99
+ let rawValue = row.value.kind === "default" ? row.value.type : row.value;
100
+ let isSensitive = false;
101
+ if (rawValue.kind === "call" && rawValue.name === "sensitive") {
102
+ isSensitive = true;
103
+ rawValue = rawValue.args[0]!;
104
+ }
105
+ const t = rawValue;
106
+ if (t.kind === "block") {
107
+ const b = entries(t);
108
+ if (b.has("family") || b.has("target") || b.has("instrument")) {
109
+ const tgt = b.get("instrument") ?? b.get("target");
110
+ let target: string | string[] | undefined;
111
+ if (tgt) {
112
+ const resolved = resolveExpr(tgt);
113
+ if (resolved.kind === "list") {
114
+ const items = resolved.items.map((i) => text(resolveExpr(i)));
115
+ target = items.length === 1 ? items[0] : items;
116
+ } else {
117
+ target = text(resolved);
118
+ }
119
+ }
120
+ return {
121
+ name: row.key,
122
+ type: "ref",
123
+ targetKind: "instrument",
124
+ ...(target !== undefined ? { target } : {}),
125
+ ...(isSensitive ? { sensitive: true } : {}),
126
+ };
127
+ }
128
+ }
129
+ const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
130
+ const f: Record<string, unknown> = {
131
+ name: row.key,
132
+ type,
133
+ ...(t.kind === "type" && t.optional ? { optional: true } : {}),
134
+ ...(isSensitive ? { sensitive: true } : {}),
135
+ };
136
+ if (type === "enum" && t.kind === "call") {
137
+ f.values = t.args.map(text);
138
+ }
139
+ if (type === "text" && t.kind === "call") {
140
+ if (t.args.length < 2 || t.args.length > 3) {
141
+ fail(
142
+ t,
143
+ "bounded text needs length bounds and an optional pattern",
144
+ "write text(1, 80)",
145
+ );
146
+ }
147
+ f.minLength = literal(resolveExpr(t.args[0]!));
148
+ f.maxLength = literal(resolveExpr(t.args[1]!));
149
+ if (t.args[2]) f.pattern = literal(resolveExpr(t.args[2]));
150
+ }
151
+ if (["integer", "money"].includes(type) && t.kind === "call") {
152
+ if (t.args.length !== 2) {
153
+ fail(
154
+ t,
155
+ "bounded fields need a minimum and maximum",
156
+ "write integer(1, 12) or money(0 SAR, 100 SAR)",
157
+ );
158
+ }
159
+ f.minimum = literal(resolveExpr(t.args[0]!));
160
+ f.maximum = literal(resolveExpr(t.args[1]!));
161
+ }
162
+ if (type === "list" && t.kind === "call") {
163
+ const item = t.args[0]!;
164
+ f.item = item.kind === "type" ? item.name : text(item);
165
+ if (item.kind === "type" && item.name === "ref") {
166
+ f.target = item.target;
167
+ f.targetKind = "object";
168
+ }
169
+ f.maxItems = t.args[1] ? literal(resolveExpr(t.args[1])) : 366;
170
+ }
171
+ if (type === "list" && t.kind === "type") {
172
+ f.item = t.target;
173
+ f.maxItems = 366;
174
+ }
175
+ if (type === "ref") {
176
+ const target =
177
+ t.kind === "type" ? t.target : (f.target as string | undefined);
178
+ if (!target && t.kind !== "block") {
179
+ fail(row, "reference needs a target", "write ref<object>");
180
+ }
181
+ f.targetKind = f.targetKind ?? "object";
182
+ if (target) f.target = target;
183
+ }
184
+ return f;
185
+ }
186
+ function lowerObjectField(
187
+ row: Entry,
188
+ resolveExpr: (e: Expr) => Expr = (e) => e,
189
+ ): UdlObjectField {
190
+ const f = lowerFieldShape(row, resolveExpr);
191
+ const constant =
192
+ row.value.kind === "default" ? resolveExpr(row.value.value) : undefined;
193
+ if (constant && !(constant.kind === "name" && constant.value === "runtime")) {
194
+ f.value =
195
+ f.type === "enum" && constant.kind === "name"
196
+ ? constant.value
197
+ : literal(constant);
198
+ }
199
+ const result = udlObjectFieldSchema.safeParse(f);
200
+ if (!result.success)
201
+ fail(
202
+ row,
203
+ result.error.message,
204
+ "use a UDL object field type and its constraints",
205
+ );
206
+ return result.data;
207
+ }
208
+ function canonicalJson(value: unknown): string {
209
+ if (value === null || typeof value !== "object") {
210
+ return JSON.stringify(value);
211
+ }
212
+ if (Array.isArray(value)) {
213
+ return `[${value.map((item) => (item === undefined ? "null" : canonicalJson(item))).join(",")}]`;
214
+ }
215
+ const record = value as Record<string, unknown>;
216
+ const keys = Object.keys(record)
217
+ .filter((k) => record[k] !== undefined)
218
+ .sort();
219
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(record[k])}`).join(",")}}`;
220
+ }
221
+
54
222
  const emptyBlock: BlockExpr = {
55
223
  kind: "block",
56
224
  entries: [],
@@ -69,9 +237,14 @@ function entries(block: BlockExpr): Map<string, Expr> {
69
237
  const previous = map.get(row.key);
70
238
  if (previous) {
71
239
  if (
72
- !["requires", "moves", "invariants", "invoke", "calculate"].includes(
73
- row.key,
74
- )
240
+ ![
241
+ "requires",
242
+ "moves",
243
+ "invariants",
244
+ "invoke",
245
+ "calculate",
246
+ "expose",
247
+ ].includes(row.key)
75
248
  )
76
249
  fail(row, `duplicate ${row.key}`, "keep one value for this name");
77
250
  const items = (expr: Expr) =>
@@ -212,12 +385,13 @@ export function compile(
212
385
  options: CompileOptions = {},
213
386
  ): CompileResult {
214
387
  const parsed = parseProgram(source);
388
+ const sources = new Map([["program", source]]);
215
389
  const diagnostic = (
216
390
  d: Diagnostic,
217
391
  stage: CompileDiagnostic["stage"],
218
392
  ): CompileDiagnostic => ({
219
393
  ...d,
220
- ...lineColAt(source, d.span.start),
394
+ ...lineColAt(sources.get(d.source ?? "program") ?? source, d.span.start),
221
395
  severity: "error",
222
396
  stage,
223
397
  });
@@ -241,6 +415,12 @@ export function compile(
241
415
  "write currency SAR or omit currency",
242
416
  );
243
417
  const templates = new Map<string, InstrumentDecl>();
418
+ const declarationSources = new Map<InstrumentDecl, string>();
419
+ const declarationExportPaths = new Map<InstrumentDecl, string>();
420
+ const requirementOrigins = new Map<
421
+ UdlSubjectRequirement,
422
+ { source: string; span: Span; message: string }
423
+ >();
244
424
  const used = new Set<string>();
245
425
  for (const use of program.decls.filter((d) => d.kind === "use")) {
246
426
  if (used.has(use.name))
@@ -255,6 +435,7 @@ export function compile(
255
435
  ).source(use.name);
256
436
  if (!content)
257
437
  fail(use, `unknown header ${use.name}`, "choose a published header");
438
+ sources.set(use.name, content);
258
439
  const header = parseProgram(content);
259
440
  if (
260
441
  header.diagnostics.length ||
@@ -266,12 +447,39 @@ export function compile(
266
447
  `header ${use.name} is malformed`,
267
448
  "repair the header source before compiling",
268
449
  );
450
+ const registerTemplates = (
451
+ parentDecl: InstrumentDecl,
452
+ prefix: string,
453
+ exportPath: string,
454
+ ) => {
455
+ templates.set(prefix, parentDecl);
456
+ declarationSources.set(parentDecl, use.name);
457
+ declarationExportPaths.set(parentDecl, exportPath);
458
+ const recs = entries(asBlock(entries(parentDecl.body).get("records")));
459
+ for (const [recName, recBlock] of recs) {
460
+ const recDecl: InstrumentDecl = {
461
+ kind: "instrument",
462
+ name: recName,
463
+ parameters: [],
464
+ body: asBlock(recBlock),
465
+ span: parentDecl.span,
466
+ };
467
+ registerTemplates(
468
+ recDecl,
469
+ `${prefix}.${recName}`,
470
+ `${exportPath}.${recName}`,
471
+ );
472
+ }
473
+ };
269
474
  for (const decl of header.program.decls)
270
- if (decl.kind === "instrument")
271
- templates.set(`${use.name}.${decl.name}`, decl);
475
+ if (decl.kind === "instrument") {
476
+ registerTemplates(decl, `${use.name}.${decl.name}`, decl.name);
477
+ }
272
478
  }
479
+ for (const decl of program.decls)
480
+ if (decl.kind === "instrument") templates.set(decl.name, decl);
273
481
  const document: UdlDocument = {
274
- udl: 3,
482
+ udl: 4,
275
483
  version: 1,
276
484
  product: program.name,
277
485
  title: program.title,
@@ -282,9 +490,12 @@ export function compile(
282
490
  programFines: { kind: "business", role: "fine_payable" },
283
491
  programCosts: { kind: "business", role: "cost_recovery" },
284
492
  },
493
+ objects: [],
285
494
  instruments: [],
286
495
  };
287
496
  const objects = new Map<string, ObjectDecl>();
497
+ const assignments = new Map<string, AssignmentDecl>();
498
+ const attachmentSubjects = new Map<string, string>();
288
499
  const names = new Set<string>();
289
500
  for (const decl of program.decls) {
290
501
  if (decl.kind === "expose" || decl.kind === "hide" || decl.kind === "use")
@@ -296,8 +507,31 @@ export function compile(
296
507
  "give this declaration a distinct name",
297
508
  );
298
509
  names.add(decl.name);
299
- if (decl.kind === "object") objects.set(decl.name, decl);
510
+ if (decl.kind === "object") {
511
+ objects.set(decl.name, decl);
512
+ for (const entry of decl.body.entries) {
513
+ const match = /^attach\s+(\w+)\s*=\s*(.+)$/.exec(entry.key);
514
+ if (!match) continue;
515
+ const name = `${decl.name}_${match[1]}`;
516
+ assignments.set(name, {
517
+ kind: "assignment",
518
+ name,
519
+ target: match[2]!,
520
+ body: asBlock(entry.value),
521
+ span: entry.span,
522
+ });
523
+ attachmentSubjects.set(name, decl.name);
524
+ }
525
+ }
526
+ if (decl.kind === "assignment") assignments.set(decl.name, decl);
300
527
  if (decl.kind === "party") {
528
+ if (subjectPartyRoles.includes(decl.name as SubjectPartyRole))
529
+ failWithCode(
530
+ decl,
531
+ "party_name_reserved",
532
+ `${decl.name} is a reserved subject role`,
533
+ "choose a party name other than owner, actor or operator",
534
+ );
301
535
  if (!["person", "business", "staff"].includes(decl.partyKind))
302
536
  fail(
303
537
  decl,
@@ -314,8 +548,81 @@ export function compile(
314
548
  const materialApprovals = new Set<UdlAction>();
315
549
  const implicitDecisions = new Map<
316
550
  string,
317
- { target: string; action: string; party: string; origin: Span }
551
+ {
552
+ target: string;
553
+ action: string;
554
+ party: string;
555
+ protectedRequest: string;
556
+ origin: Span;
557
+ }
318
558
  >();
559
+ const resolveFamily = (
560
+ rawPath: string,
561
+ expr: { span: Span; source?: string },
562
+ required = true,
563
+ ): UdlFamily | undefined => {
564
+ const parts = rawPath.split(".");
565
+ if (parts.length < 2) {
566
+ if (!required) return undefined;
567
+ failWithCode(
568
+ expr,
569
+ "HSX1001",
570
+ `invalid family ${rawPath}`,
571
+ "use module.instrument or module.instrument.record",
572
+ );
573
+ }
574
+ const moduleName = parts[0]!;
575
+ const exportPath = parts.slice(1).join(".");
576
+ const targetTemplate = templates.get(rawPath);
577
+ if (!targetTemplate) {
578
+ if (!required) return undefined;
579
+ failWithCode(
580
+ expr,
581
+ "HSX1001",
582
+ `unknown family declaration ${rawPath}`,
583
+ "choose a declared standard instrument",
584
+ );
585
+ }
586
+ const topTemplate = templates.get(`${moduleName}.${parts[1]!}`);
587
+ if (!topTemplate) {
588
+ if (!required) return undefined;
589
+ failWithCode(
590
+ expr,
591
+ "HSX1001",
592
+ `unknown family declaration ${moduleName}.${parts[1]!}`,
593
+ "choose a declared standard instrument",
594
+ );
595
+ }
596
+ const topBody = entries(topTemplate.body);
597
+ let revision: number | undefined;
598
+ if (topBody.has("familyRevision")) {
599
+ const val = literal(topBody.get("familyRevision")!);
600
+ if (typeof val === "number") revision = val;
601
+ }
602
+ let currentBody = topTemplate.body;
603
+ for (const recName of parts.slice(2)) {
604
+ const recs = entries(asBlock(entries(currentBody).get("records")));
605
+ const child = recs.get(recName);
606
+ if (child) {
607
+ currentBody = asBlock(child);
608
+ const cBody = entries(currentBody);
609
+ if (cBody.has("familyRevision")) {
610
+ const val = literal(cBody.get("familyRevision")!);
611
+ if (typeof val === "number") revision = val;
612
+ }
613
+ }
614
+ }
615
+ if (revision === undefined) {
616
+ if (!required) return undefined;
617
+ failWithCode(
618
+ expr,
619
+ "HSX1001",
620
+ `declaration ${rawPath} has no familyRevision declared`,
621
+ "declare familyRevision on the standard instrument",
622
+ );
623
+ }
624
+ return { module: moduleName, exportPath, revision };
625
+ };
319
626
  const addInstrument = (
320
627
  decl: InstrumentDecl,
321
628
  id: string,
@@ -324,7 +631,169 @@ export function compile(
324
631
  inherited = new Map<string, Expr>(),
325
632
  inheritedApprovers = new Set<string>(),
326
633
  inheritedEnums = new Map<string, string[]>(),
634
+ attachmentInfo?: {
635
+ subjectKindId: string;
636
+ attachmentName: string;
637
+ renames: Map<string, string>;
638
+ exposed: Map<string, string>;
639
+ parties: Record<string, AttachmentPartyBinding>;
640
+ },
641
+ familyDeclaration?: {
642
+ module: string;
643
+ exportPath: string;
644
+ revision?: number | undefined;
645
+ },
327
646
  ) => {
647
+ const resolveFamilyInstruments = (
648
+ family: UdlFamily,
649
+ currentInstId?: string,
650
+ expr?: { span: Span; source?: string },
651
+ ): string[] => {
652
+ const found = new Set<string>();
653
+ if (currentInstId && familyDeclaration) {
654
+ if (
655
+ family.module === familyDeclaration.module &&
656
+ family.exportPath.startsWith(`${familyDeclaration.exportPath}.`)
657
+ ) {
658
+ const sub = family.exportPath
659
+ .slice(familyDeclaration.exportPath.length + 1)
660
+ .replaceAll(".", "_");
661
+ found.add(`${currentInstId}_${sub}`);
662
+ }
663
+ }
664
+ const topDeclName = family.exportPath.split(".")[0]!;
665
+ const subRecordPath = family.exportPath.includes(".")
666
+ ? family.exportPath.slice(topDeclName.length + 1).replaceAll(".", "_")
667
+ : undefined;
668
+ for (const asgn of assignments.values()) {
669
+ if (asgn.target === `${family.module}.${topDeclName}`) {
670
+ if (subRecordPath) {
671
+ found.add(`${asgn.name}_${subRecordPath}`);
672
+ } else {
673
+ found.add(asgn.name);
674
+ }
675
+ }
676
+ }
677
+ for (const inst of document.instruments) {
678
+ if (
679
+ inst.family &&
680
+ inst.family.module === family.module &&
681
+ inst.family.exportPath === family.exportPath &&
682
+ inst.family.revision === family.revision
683
+ ) {
684
+ found.add(inst.id);
685
+ }
686
+ }
687
+ const result = [...found];
688
+ if (result.length === 0 && expr) {
689
+ failWithCode(
690
+ expr,
691
+ "HSX1001",
692
+ `no instruments found for family ${family.module}.${family.exportPath}`,
693
+ "declare an attachment matching this family",
694
+ );
695
+ }
696
+ return result;
697
+ };
698
+
699
+ const resolveChildExportPath = (
700
+ parentDecl: InstrumentDecl,
701
+ suffix: string,
702
+ ): string | undefined => {
703
+ const recs = entries(asBlock(entries(parentDecl.body).get("records")));
704
+ for (const [recName] of recs) {
705
+ if (suffix === recName) return recName;
706
+ }
707
+ for (const [recName, recBlock] of recs) {
708
+ if (suffix.startsWith(`${recName}_`)) {
709
+ const rest = resolveChildExportPath(
710
+ {
711
+ kind: "instrument",
712
+ name: recName,
713
+ parameters: [],
714
+ body: asBlock(recBlock),
715
+ span: parentDecl.span,
716
+ },
717
+ suffix.slice(recName.length + 1),
718
+ );
719
+ if (rest) return `${recName}.${rest}`;
720
+ }
721
+ }
722
+ return undefined;
723
+ };
724
+
725
+ const getInstrumentFamily = (targetId: string): UdlFamily | undefined => {
726
+ const existing = document.instruments.find((i) => i.id === targetId);
727
+ if (existing?.family) return existing.family;
728
+
729
+ if (targetId.startsWith(`${id}_`) && familyDeclaration) {
730
+ const sub = resolveChildExportPath(
731
+ decl,
732
+ targetId.slice(id.length + 1),
733
+ );
734
+ if (sub) {
735
+ const fullExport = `${familyDeclaration.exportPath}.${sub}`;
736
+ try {
737
+ return resolveFamily(
738
+ `${familyDeclaration.module}.${fullExport}`,
739
+ { span: origin },
740
+ false,
741
+ );
742
+ } catch {}
743
+ }
744
+ }
745
+
746
+ for (const [asgnName, asgn] of assignments) {
747
+ if (targetId === asgnName || targetId.startsWith(`${asgnName}_`)) {
748
+ const tmpl = templates.get(asgn.target);
749
+ if (!tmpl) continue;
750
+ const mod = declarationSources.get(tmpl);
751
+ if (!mod || mod === "program") continue;
752
+ if (targetId === asgnName) {
753
+ try {
754
+ return resolveFamily(asgn.target, asgn, false);
755
+ } catch {}
756
+ } else {
757
+ const sub = resolveChildExportPath(
758
+ tmpl,
759
+ targetId.slice(asgnName.length + 1),
760
+ );
761
+ if (sub) {
762
+ const fullTarget = `${asgn.target}.${sub}`;
763
+ try {
764
+ return resolveFamily(fullTarget, asgn, false);
765
+ } catch {}
766
+ }
767
+ }
768
+ }
769
+ }
770
+ return undefined;
771
+ };
772
+
773
+ const checkTargetFamily = (
774
+ targetIds: string | string[],
775
+ expectedFamily: UdlFamily,
776
+ expr: { span: Span; source?: string },
777
+ ): void => {
778
+ const ids = Array.isArray(targetIds) ? targetIds : [targetIds];
779
+ for (const tid of ids) {
780
+ const fam = getInstrumentFamily(tid);
781
+ if (
782
+ !fam ||
783
+ fam.module !== expectedFamily.module ||
784
+ fam.exportPath !== expectedFamily.exportPath ||
785
+ fam.revision !== expectedFamily.revision
786
+ ) {
787
+ failWithCode(
788
+ expr,
789
+ "HSX1001",
790
+ `target instrument ${tid} family does not match expected family ${expectedFamily.module}.${expectedFamily.exportPath} (revision ${expectedFamily.revision})`,
791
+ "ensure target instrument matches the declared family",
792
+ );
793
+ }
794
+ }
795
+ };
796
+
328
797
  const enums = new Map(inheritedEnums);
329
798
  for (const parameter of decl.parameters) {
330
799
  const type =
@@ -335,18 +804,44 @@ export function compile(
335
804
  enums.set(parameter.key, type.args.map(text));
336
805
  }
337
806
  const approvers = new Set(inheritedApprovers);
338
- const supplied = entries(arguments_);
807
+ const supplied = new Map<string, Expr>();
808
+ for (const entry of arguments_.entries) {
809
+ if (supplied.has(entry.key))
810
+ fail(
811
+ entry,
812
+ `duplicate tunable ${entry.key}`,
813
+ "supply each parameter once",
814
+ );
815
+ supplied.set(entry.key, entry.value);
816
+ }
339
817
  const environment = new Map<string, Expr>(inherited);
340
818
  for (const param of decl.parameters) {
341
819
  const type =
342
820
  param.value.kind === "default" ? param.value.type : param.value;
343
821
  const fallback =
344
822
  param.value.kind === "default" ? param.value.value : undefined;
345
- const actual = supplied.get(param.key) ?? fallback;
823
+ const typeName =
824
+ type.kind === "type" || type.kind === "call" ? type.name : text(type);
825
+ const partyParameter =
826
+ attachmentInfo && (typeName === "party" || typeName === "approval");
827
+ const byName: Expr | undefined =
828
+ partyParameter &&
829
+ (subjectPartyRoles.includes(param.key as SubjectPartyRole) ||
830
+ document.parties[param.key])
831
+ ? { kind: "name", value: param.key, span: origin }
832
+ : undefined;
833
+ const actual =
834
+ supplied.get(param.key) ??
835
+ byName ??
836
+ (fallback && {
837
+ ...fallback,
838
+ source: declarationSources.get(decl) ?? "program",
839
+ });
346
840
  if (!actual) {
347
841
  if (type.kind === "type" && type.optional) continue;
348
- fail(
842
+ failWithCode(
349
843
  { span: origin },
844
+ partyParameter ? "subject_party_unbound" : "HSX1001",
350
845
  `${id} needs ${param.key}`,
351
846
  `add ${param.key}: value inside ${id}`,
352
847
  );
@@ -360,7 +855,16 @@ export function compile(
360
855
  `unknown tunable ${key}`,
361
856
  `choose ${decl.parameters.map((p) => p.key).join(", ")}`,
362
857
  );
363
- const resolve = (expr: Expr, seen = new Set<string>()): Expr => {
858
+ const isParty = (name: string) =>
859
+ !!document.parties[name] ||
860
+ (!!attachmentInfo &&
861
+ subjectPartyRoles.includes(name as SubjectPartyRole));
862
+ const resolvedParties = new Set<string>();
863
+ const resolve = (
864
+ expr: Expr,
865
+ seen = new Set<string>(),
866
+ partyBinding = false,
867
+ ): Expr => {
364
868
  if (
365
869
  expr.kind === "call" &&
366
870
  ["object", "all", "party"].includes(expr.name)
@@ -375,22 +879,33 @@ export function compile(
375
879
  const matches =
376
880
  expr.name === "party"
377
881
  ? Object.entries(document.parties)
378
- .filter(([, party]) => party.kind === type)
882
+ .filter(
883
+ ([name, party]) =>
884
+ party.kind === type && (!partyBinding || names.has(name)),
885
+ )
379
886
  .map(([name]) => name)
380
- : [...objects.values()]
887
+ : [...assignments.values()]
888
+ .filter(
889
+ (assignment) =>
890
+ expr.name === "all" ||
891
+ !attachmentSubjects.has(assignment.name) ||
892
+ attachmentSubjects.get(assignment.name) ===
893
+ attachmentInfo?.subjectKindId,
894
+ )
381
895
  .filter(
382
- (object) =>
383
- object.object === type ||
384
- type.startsWith(`${object.object}.`),
896
+ (assignment) =>
897
+ assignment.target === type ||
898
+ type.startsWith(`${assignment.target}.`),
385
899
  )
386
900
  .map(
387
- (object) =>
388
- object.name +
389
- type.slice(object.object.length).replaceAll(".", "_"),
901
+ (assignment) =>
902
+ assignment.name +
903
+ type.slice(assignment.target.length).replaceAll(".", "_"),
390
904
  );
391
905
  if (expr.name !== "all" && matches.length !== 1)
392
- fail(
393
- { span: origin },
906
+ failWithCode(
907
+ expr,
908
+ partyBinding ? "subject_party_unbound" : "HSX1001",
394
909
  `${id} needs ${expr.name === "all" ? "at least one" : "exactly one"} ${type}`,
395
910
  "declare the required object or supply this tunable explicitly",
396
911
  );
@@ -404,25 +919,46 @@ export function compile(
404
919
  : items[0]!;
405
920
  }
406
921
  if (expr.kind !== "name") return expr;
922
+ const binding = environment.get(expr.value);
923
+ if (
924
+ attachmentInfo &&
925
+ subjectPartyRoles.includes(expr.value as SubjectPartyRole) &&
926
+ (!binding ||
927
+ (binding.kind === "name" && binding.value === expr.value))
928
+ )
929
+ return expr;
930
+ if (attachmentInfo) {
931
+ const [local, ...tail] = expr.value.split(".");
932
+ const target = `${attachmentInfo.subjectKindId}_${local}`;
933
+ if (attachmentSubjects.has(target))
934
+ return { ...expr, value: [target, ...tail].join("_") };
935
+ }
407
936
  if (expr.value.startsWith("party.")) {
408
- const binding = environment.get(expr.value.slice(6));
409
- if (binding?.kind === "name" && document.parties[binding.value])
410
- return { ...expr, value: `party.${binding.value}` };
937
+ const [, party, ...members] = expr.value.split(".");
938
+ const binding = environment.get(party!);
939
+ if (binding?.kind === "name" && isParty(binding.value))
940
+ return {
941
+ ...expr,
942
+ value: ["party", binding.value, ...members].join("."),
943
+ };
411
944
  }
412
945
  const [root, ...tail] = expr.value.split(".");
413
946
  const bound = environment.get(root!);
414
947
  if (!bound || (bound.kind === "name" && bound.value === root))
415
948
  return expr;
416
949
  if (seen.has(root!))
417
- return fail(
950
+ return failWithCode(
418
951
  expr,
952
+ partyBinding ? "subject_party_unbound" : "HSX1001",
419
953
  `cyclic tunable ${root}`,
420
954
  "replace the cycle with a literal or declared reference",
421
955
  );
422
956
  let resolved =
423
- supplied.has(root!) || inherited.has(root!) || enums.has(root!)
957
+ resolvedParties.has(root!) ||
958
+ (!partyBinding &&
959
+ (supplied.has(root!) || inherited.has(root!) || enums.has(root!)))
424
960
  ? bound
425
- : resolve(bound, new Set([...seen, root!]));
961
+ : resolve(bound, new Set([...seen, root!]), partyBinding);
426
962
  for (const key of tail) {
427
963
  if (resolved.kind !== "block") return expr;
428
964
  const child = entries(resolved).get(key);
@@ -432,7 +968,7 @@ export function compile(
432
968
  `missing tunable ${expr.value}`,
433
969
  `declare ${key} in ${root}`,
434
970
  );
435
- resolved = resolve(child, new Set([...seen, root!]));
971
+ resolved = resolve(child, new Set([...seen, root!]), partyBinding);
436
972
  }
437
973
  return resolved;
438
974
  };
@@ -443,7 +979,13 @@ export function compile(
443
979
  param.value.kind === "default" ? param.value.type : param.value;
444
980
  const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
445
981
  const v =
446
- supplied.has(param.key) || type === "enum" ? actual : resolve(actual);
982
+ (supplied.has(param.key) && !attachmentInfo) || type === "enum"
983
+ ? actual
984
+ : resolve(
985
+ actual,
986
+ new Set(),
987
+ !!attachmentInfo && (type === "party" || type === "approval"),
988
+ );
447
989
  environment.set(param.key, v);
448
990
  if (type === "enum" && t.kind === "call") {
449
991
  if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
@@ -456,12 +998,35 @@ export function compile(
456
998
  if (v.kind !== "list")
457
999
  fail(v, `${param.key} needs a list`, "write [value, value]");
458
1000
  } else if (type === "party" || type === "approval") {
459
- if (v.kind !== "name" || !document.parties[v.value])
460
- fail(
461
- v,
1001
+ if (v.kind !== "name" || !isParty(v.value))
1002
+ failWithCode(
1003
+ actual,
1004
+ attachmentInfo ? "subject_party_unbound" : "HSX1001",
462
1005
  `${param.key} needs a declared party`,
463
1006
  "declare a party and use its name here",
464
1007
  );
1008
+ const party = document.parties[v.value];
1009
+ if (
1010
+ (type === "approval" && (party?.kind !== "staff" || !party.role)) ||
1011
+ (type === "party" &&
1012
+ (party?.kind === "staff" ||
1013
+ (attachmentInfo && party?.kind === "person")))
1014
+ )
1015
+ failWithCode(
1016
+ actual,
1017
+ "party_kind_mismatch",
1018
+ `${param.key} cannot bind ${v.value}`,
1019
+ type === "approval"
1020
+ ? "use a declared staff party with a role"
1021
+ : "use a subject role or declared business",
1022
+ );
1023
+ resolvedParties.add(param.key);
1024
+ if (attachmentInfo)
1025
+ attachmentInfo.parties[param.key] = subjectPartyRoles.includes(
1026
+ v.value as SubjectPartyRole,
1027
+ )
1028
+ ? { role: v.value as SubjectPartyRole }
1029
+ : { party: v.value };
465
1030
  if (type === "approval") approvers.add(text(v));
466
1031
  } else if (type === "ref") {
467
1032
  const values = v.kind === "list" ? v.items : [v];
@@ -487,14 +1052,17 @@ export function compile(
487
1052
  );
488
1053
  const [root, ...tail] = value.value.split(".");
489
1054
  const obj = objects.get(root!);
1055
+ const assignment = assignments.get(root!);
1056
+ const targetType = obj ? obj.name : assignment?.target;
490
1057
  if (
491
1058
  (!obj &&
1059
+ !assignment &&
492
1060
  !document.instruments.some(
493
1061
  (inst) => inst.id === value.value,
494
1062
  )) ||
495
1063
  (t.kind === "type" &&
496
1064
  t.target &&
497
- [obj?.object, ...tail].join(".") !== t.target)
1065
+ [targetType, ...tail].join(".") !== t.target)
498
1066
  )
499
1067
  fail(
500
1068
  value,
@@ -590,12 +1158,15 @@ export function compile(
590
1158
  for (const key of body.keys())
591
1159
  if (
592
1160
  ![
1161
+ "familyRevision",
593
1162
  "fields",
594
1163
  "lifecycle",
595
1164
  "records",
596
1165
  "summary",
597
1166
  "invariants",
598
1167
  "constraints",
1168
+ "reports",
1169
+ "revisioned",
599
1170
  ].includes(key) &&
600
1171
  !key.startsWith("action ")
601
1172
  )
@@ -604,6 +1175,24 @@ export function compile(
604
1175
  `unknown instrument clause ${key}`,
605
1176
  "use fields, lifecycle, actions, invariants, or records",
606
1177
  );
1178
+ if (body.has("familyRevision")) {
1179
+ const revExpr = body.get("familyRevision")!;
1180
+ const revVal = literal(revExpr);
1181
+ if (
1182
+ typeof revVal !== "number" ||
1183
+ !Number.isInteger(revVal) ||
1184
+ revVal <= 0
1185
+ ) {
1186
+ fail(
1187
+ revExpr,
1188
+ "familyRevision must be a positive integer",
1189
+ "use a positive integer revision",
1190
+ );
1191
+ }
1192
+ if (familyDeclaration) {
1193
+ familyDeclaration.revision = revVal;
1194
+ }
1195
+ }
607
1196
  const records = entries(asBlock(body.get("records")));
608
1197
  if (!inherited.size)
609
1198
  environment.set("parent", { kind: "name", value: id, span: origin });
@@ -616,11 +1205,63 @@ export function compile(
616
1205
  });
617
1206
  const fields: UdlField[] = [];
618
1207
  const calculations: UdlCalculation[] = [];
1208
+ let currentAction: UdlAction | undefined;
1209
+ let currentActionName: string | undefined;
619
1210
  const path = (expr: Expr): string => {
620
1211
  const value = resolve(expr);
621
1212
  const name = text(value);
622
- if (document.parties[name]) return `party.${name}`;
623
- return /^(self|input|party)\./.test(name) ? name : `self.${name}`;
1213
+ if (isParty(name)) return `party.${name}`;
1214
+ if (name.startsWith("subject.")) {
1215
+ const subField = name.split(".")[1]!;
1216
+ if (currentAction) {
1217
+ const req = currentAction.subject?.requirements.find(
1218
+ (r) => r.field.name === subField,
1219
+ );
1220
+ if (!req) {
1221
+ failWithCode(
1222
+ expr,
1223
+ "subject_field_unknown",
1224
+ `subject.${subField} names no declared subject requirement in action ${currentActionName}`,
1225
+ `declare ${subField} in subject { ... }`,
1226
+ );
1227
+ }
1228
+ } else {
1229
+ const declaredInAction = decl.body.entries.some((e) => {
1230
+ if (!e.key.startsWith("action ")) return false;
1231
+ const subBlock = entries(asBlock(e.value)).get("subject");
1232
+ if (!subBlock) return false;
1233
+ return asBlock(subBlock).entries.some((se) => {
1234
+ if (se.key === subField) return true;
1235
+ if (se.key === "adapter") {
1236
+ const names =
1237
+ se.value.kind === "list"
1238
+ ? se.value.items.map(text)
1239
+ : [text(se.value)];
1240
+ return names.some((n) => {
1241
+ const reg = options.adapterRegistry?.[n];
1242
+ if (!reg) return false;
1243
+ const op = reg.adapter.operationMap[reg.operation];
1244
+ return op?.subjectRequirements?.some(
1245
+ (sr) => sr.name === subField,
1246
+ );
1247
+ });
1248
+ }
1249
+ return false;
1250
+ });
1251
+ });
1252
+ if (!declaredInAction) {
1253
+ failWithCode(
1254
+ expr,
1255
+ "subject_field_unknown",
1256
+ `subject.${subField} names no declared subject requirement in instrument ${id}`,
1257
+ `declare ${subField} in an action subject { ... }`,
1258
+ );
1259
+ }
1260
+ }
1261
+ }
1262
+ return /^(self|input|party|subject)\./.test(name)
1263
+ ? name
1264
+ : `self.${name}`;
624
1265
  };
625
1266
  const val = (expr: Expr): UdlValue => {
626
1267
  const v = resolve(expr);
@@ -637,8 +1278,57 @@ export function compile(
637
1278
  return id;
638
1279
  const value = resolve(expr);
639
1280
  if (value.kind === "block") {
1281
+ const rawEntries = entries(value);
1282
+ if (
1283
+ (rawEntries.has("family") && !rawEntries.has("kind")) ||
1284
+ ((rawEntries.has("states") ||
1285
+ rawEntries.has("reference") ||
1286
+ rawEntries.has("anchor")) &&
1287
+ rawEntries.has("instrument"))
1288
+ ) {
1289
+ let famTuple: UdlFamily | undefined;
1290
+ if (rawEntries.has("family")) {
1291
+ const famExpr = rawEntries.get("family")!;
1292
+ const famStr =
1293
+ famExpr.kind === "name" ? famExpr.value : text(famExpr);
1294
+ famTuple = resolveFamily(famStr, famExpr);
1295
+ }
1296
+
1297
+ let instrumentVal: unknown;
1298
+ if (rawEntries.has("instrument")) {
1299
+ instrumentVal = data(rawEntries.get("instrument")!);
1300
+ if (famTuple) {
1301
+ checkTargetFamily(
1302
+ instrumentVal as string | string[],
1303
+ famTuple,
1304
+ rawEntries.get("instrument")!,
1305
+ );
1306
+ }
1307
+ } else if (famTuple) {
1308
+ const matched = resolveFamilyInstruments(
1309
+ famTuple,
1310
+ id,
1311
+ rawEntries.get("family")!,
1312
+ );
1313
+ instrumentVal = matched.length === 1 ? matched[0] : matched;
1314
+ }
1315
+
1316
+ const result: Record<string, unknown> = {};
1317
+ if (famTuple) {
1318
+ result.family = famTuple;
1319
+ }
1320
+ if (instrumentVal !== undefined) {
1321
+ result.instrument = instrumentVal;
1322
+ }
1323
+ for (const [k, v] of rawEntries) {
1324
+ if (k === "family" || k === "instrument") continue;
1325
+ result[k] = data(v);
1326
+ }
1327
+ return result;
1328
+ }
1329
+
640
1330
  const result = Object.fromEntries(
641
- [...entries(value)].map(([key, value]) => [key, data(value)]),
1331
+ [...rawEntries].map(([key, value]) => [key, data(value)]),
642
1332
  );
643
1333
  const selection = result.selection as
644
1334
  | { instrument?: unknown }
@@ -690,55 +1380,86 @@ export function compile(
690
1380
  const t = row.value.kind === "default" ? row.value.type : row.value;
691
1381
  const constant =
692
1382
  row.value.kind === "default" ? resolve(row.value.value) : undefined;
693
- const type =
694
- t.kind === "type" || t.kind === "call" ? t.name : text(t);
695
- const f: Record<string, unknown> = {
696
- name: row.key,
697
- type,
698
- ...(t.kind === "type" && t.optional ? { optional: true } : {}),
699
- };
700
- if (type === "enum" && t.kind === "call") f.values = t.args.map(text);
701
- if (["integer", "money"].includes(type) && t.kind === "call") {
702
- if (t.args.length !== 2)
703
- fail(
704
- t,
705
- "bounded fields need a minimum and maximum",
706
- "write integer(1, 12) or money(0 SAR, 100 SAR)",
707
- );
708
- f.minimum = literal(resolve(t.args[0]!));
709
- f.maximum = literal(resolve(t.args[1]!));
710
- }
1383
+ const f = lowerFieldShape(row, resolve);
1384
+ const type = f.type;
711
1385
  if (type === "list" && t.kind === "call") {
712
- f.item = text(t.args[0]!);
713
- f.maxItems = t.args[1] ? literal(resolve(t.args[1])) : 366;
714
- }
715
- if (type === "list" && t.kind === "type") {
716
- f.item = t.target;
717
- f.maxItems = 366;
1386
+ const item = t.args[0]!;
1387
+ if (item.kind === "type" && item.name === "ref" && item.target) {
1388
+ f.target = text(
1389
+ resolve({ kind: "name", value: item.target, span: item.span }),
1390
+ ).replaceAll(".", "_");
1391
+ f.targetKind = objects.has(String(f.target))
1392
+ ? "object"
1393
+ : "instrument";
1394
+ }
718
1395
  }
719
1396
  if (type === "ref") {
720
- const target = t.kind === "type" ? t.target : undefined;
721
- if (!target)
722
- fail(row, "reference needs a target", "write ref<object>");
723
- const [root, ...tail] = target.split(".");
724
- const resolved = environment.get(root!);
725
- const resolvedTargets =
726
- resolved?.kind === "list"
727
- ? resolved.items
728
- : resolved
729
- ? [resolved]
730
- : [];
731
- const targets = resolvedTargets.map((value) =>
732
- [text(resolve(value)).replaceAll(".", "_"), ...tail].join("_"),
733
- );
734
- f.target =
735
- target === "self"
736
- ? id
737
- : targets.length === 1
738
- ? targets[0]
739
- : targets.length
740
- ? targets
741
- : target;
1397
+ if (t.kind === "block") {
1398
+ const b = entries(t);
1399
+ const famNode = b.get("targetFamily") ?? b.get("family");
1400
+ let targetFamTuple: UdlFamily | undefined;
1401
+ if (famNode) {
1402
+ const famStr =
1403
+ famNode.kind === "name" ? famNode.value : text(famNode);
1404
+ targetFamTuple = resolveFamily(famStr, famNode);
1405
+ f.targetFamily = targetFamTuple;
1406
+ }
1407
+ if (b.has("target") || b.has("instrument")) {
1408
+ const tgtExpr = (b.get("target") ?? b.get("instrument"))!;
1409
+ const resolvedTgt = resolve(tgtExpr);
1410
+ let tgtVal: string | string[];
1411
+ if (resolvedTgt.kind === "list") {
1412
+ const items = resolvedTgt.items.map((it) =>
1413
+ text(resolve(it)).replaceAll(".", "_"),
1414
+ );
1415
+ tgtVal = items.length === 1 ? items[0]! : items;
1416
+ } else {
1417
+ tgtVal = text(resolvedTgt).replaceAll(".", "_");
1418
+ }
1419
+ f.target = tgtVal;
1420
+ if (targetFamTuple) {
1421
+ checkTargetFamily(tgtVal, targetFamTuple, tgtExpr);
1422
+ }
1423
+ } else if (targetFamTuple) {
1424
+ const matched = resolveFamilyInstruments(
1425
+ targetFamTuple,
1426
+ id,
1427
+ famNode!,
1428
+ );
1429
+ f.target = matched.length === 1 ? matched[0] : matched;
1430
+ }
1431
+ f.targetKind =
1432
+ typeof f.target === "string" && objects.has(f.target)
1433
+ ? "object"
1434
+ : "instrument";
1435
+ } else {
1436
+ const target = t.kind === "type" ? t.target : undefined;
1437
+ if (!target)
1438
+ fail(row, "reference needs a target", "write ref<object>");
1439
+ const [root, ...tail] = target.split(".");
1440
+ const resolved = environment.get(root!);
1441
+ const resolvedTargets =
1442
+ resolved?.kind === "list"
1443
+ ? resolved.items
1444
+ : resolved
1445
+ ? [resolved]
1446
+ : [];
1447
+ const targets = resolvedTargets.map((value) =>
1448
+ [text(resolve(value)).replaceAll(".", "_"), ...tail].join("_"),
1449
+ );
1450
+ f.target =
1451
+ target === "self"
1452
+ ? id
1453
+ : targets.length === 1
1454
+ ? targets[0]
1455
+ : targets.length
1456
+ ? targets
1457
+ : target;
1458
+ f.targetKind =
1459
+ typeof f.target === "string" && objects.has(f.target)
1460
+ ? "object"
1461
+ : "instrument";
1462
+ }
742
1463
  } else if (type === "account") {
743
1464
  if (t.kind === "call") {
744
1465
  if (t.args.length < 1 || t.args.length > 4)
@@ -751,7 +1472,13 @@ export function compile(
751
1472
  f.book = t.args[1] ? text(t.args[1]) : "cash";
752
1473
  if (t.args[2]) {
753
1474
  const mode = text(t.args[2]);
754
- if (["contra", "external"].includes(mode)) f[mode] = true;
1475
+ if (mode === "external")
1476
+ fail(
1477
+ t,
1478
+ "external account mode was removed",
1479
+ "use a reservation and instruction-bound evidence",
1480
+ );
1481
+ if (mode === "contra") f.contra = true;
755
1482
  else f.key = mode;
756
1483
  }
757
1484
  if (t.args[3]) f.key = text(t.args[3]);
@@ -876,6 +1603,7 @@ export function compile(
876
1603
  lifecycle.transitions = {};
877
1604
  const inst: UdlInstrument = {
878
1605
  id,
1606
+ ...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
879
1607
  title: title(id),
880
1608
  summary: body.has("summary")
881
1609
  ? String(data(body.get("summary")!))
@@ -894,20 +1622,53 @@ export function compile(
894
1622
  ...block,
895
1623
  entries: block.entries.flatMap((entry) => {
896
1624
  if (!entry.key.startsWith("when ")) return [entry];
897
- const [, tunable, , choice] = entry.key.split(" ");
1625
+ const [, tunable, relation, choice] = entry.key.split(" ");
898
1626
  const binding = environment.get(tunable!);
899
1627
  if (!binding)
900
1628
  fail(
901
1629
  entry,
902
1630
  `unknown branch tunable ${tunable}`,
903
- "name an enum tunable declared by this header",
1631
+ "name an enum or reference tunable declared by this header",
904
1632
  );
905
- if (!enums.get(tunable!)?.includes(choice!))
906
- fail(
907
- entry,
908
- `unknown enum branch ${choice}`,
909
- "use a declared enum value",
1633
+ let matches: boolean;
1634
+ if (relation === "has") {
1635
+ // Inspect declarations, not lowering order: a bound object may
1636
+ // appear after the instrument that asks about its fields.
1637
+ const bound = resolve(binding!);
1638
+ const [root, ...children] = text(bound).split(".");
1639
+ const assignment = assignments.get(root!);
1640
+ let target = assignment
1641
+ ? templates.get(assignment.target)?.body
1642
+ : program.decls
1643
+ .filter(
1644
+ (decl): decl is InstrumentDecl =>
1645
+ decl.kind === "instrument",
1646
+ )
1647
+ .find((decl) => decl.name === root)?.body;
1648
+ for (const child of children) {
1649
+ const record = target
1650
+ ? entries(asBlock(entries(target).get("records"))).get(child)
1651
+ : undefined;
1652
+ target = record ? asBlock(record) : undefined;
1653
+ }
1654
+ if (!target)
1655
+ fail(
1656
+ entry,
1657
+ "field branch needs a declared object",
1658
+ "bind a reference to a declared object",
1659
+ );
1660
+ matches = entries(asBlock(entries(target!).get("fields"))).has(
1661
+ choice!,
910
1662
  );
1663
+ } else {
1664
+ if (!enums.get(tunable!)?.includes(choice!))
1665
+ fail(
1666
+ entry,
1667
+ `unknown enum branch ${choice}`,
1668
+ "use a declared enum value",
1669
+ );
1670
+ matches = text(binding!) === choice;
1671
+ }
911
1672
  const body = asBlock(entry.value);
912
1673
  for (const clause of body.entries)
913
1674
  if (
@@ -921,10 +1682,225 @@ export function compile(
921
1682
  "when permits requirements, calculations, moves and invocations",
922
1683
  "keep lifecycle and actor clauses outside the branch",
923
1684
  );
924
- return text(binding) === choice ? selected(body).entries : [];
1685
+ return matches ? selected(body).entries : [];
925
1686
  }),
926
1687
  });
927
1688
  const slots = entries(selected(asBlock(row.value)));
1689
+ let actionSubject: UdlActionSubject | undefined;
1690
+ let subjectExpr = slots.get("subject");
1691
+ const boundaryBindings = new Set<string>();
1692
+ const authoredMoves = slots.get("moves");
1693
+ for (const move of authoredMoves?.kind === "list"
1694
+ ? authoredMoves.items
1695
+ : authoredMoves
1696
+ ? [authoredMoves]
1697
+ : []) {
1698
+ const parts = entries(asBlock(move));
1699
+ const boundary = parts.get("boundary");
1700
+ if (!boundary) continue;
1701
+ if (
1702
+ (parts.has("operation")
1703
+ ? String(data(parts.get("operation")!))
1704
+ : "internal_transfer.create") !== "internal_transfer.reserve" ||
1705
+ parts.has("shares") ||
1706
+ parts.has("fee")
1707
+ )
1708
+ fail(
1709
+ move,
1710
+ "boundary dispatch requires a reservation",
1711
+ "reserve the exact amount before dispatch",
1712
+ );
1713
+ const adapterExpr = entries(asBlock(boundary)).get("adapter");
1714
+ if (!adapterExpr)
1715
+ fail(
1716
+ boundary,
1717
+ "boundary needs an adapter",
1718
+ "name a bound ADL adapter",
1719
+ );
1720
+ const binding = text(resolve(adapterExpr!));
1721
+ const target =
1722
+ options.adapterRegistry &&
1723
+ Object.hasOwn(options.adapterRegistry, binding)
1724
+ ? options.adapterRegistry[binding]
1725
+ : undefined;
1726
+ if (
1727
+ !target ||
1728
+ !Object.hasOwn(target.adapter.operationMap, target.operation)
1729
+ )
1730
+ fail(
1731
+ boundary,
1732
+ `unknown boundary adapter ${binding}`,
1733
+ "bind the named ADL adapter before compilation",
1734
+ );
1735
+ boundaryBindings.add(binding);
1736
+ }
1737
+ if (boundaryBindings.size) {
1738
+ const block = asBlock(subjectExpr);
1739
+ const declared = new Set(
1740
+ block.entries
1741
+ .filter((entry) => entry.key === "adapter")
1742
+ .flatMap((entry) =>
1743
+ entry.value.kind === "list"
1744
+ ? entry.value.items.map(text)
1745
+ : [text(entry.value)],
1746
+ ),
1747
+ );
1748
+ subjectExpr = {
1749
+ ...block,
1750
+ entries: [
1751
+ ...block.entries,
1752
+ ...[...boundaryBindings]
1753
+ .filter((binding) => !declared.has(binding))
1754
+ .map((binding) => ({
1755
+ key: "adapter",
1756
+ value: {
1757
+ kind: "name" as const,
1758
+ value: binding,
1759
+ span: row.span,
1760
+ },
1761
+ span: row.span,
1762
+ })),
1763
+ ],
1764
+ };
1765
+ }
1766
+ if (subjectExpr) {
1767
+ const subjectBlock = asBlock(subjectExpr);
1768
+ const directRequirements: UdlSubjectRequirement[] = [];
1769
+ const adapterList: UdlActionSubject["adapters"] = [];
1770
+ for (const entry of subjectBlock.entries) {
1771
+ if (entry.key === "adapter") {
1772
+ const bindingNames =
1773
+ entry.value.kind === "list"
1774
+ ? entry.value.items.map(text)
1775
+ : [text(entry.value)];
1776
+ for (const bindingName of bindingNames) {
1777
+ const target = options.adapterRegistry?.[bindingName];
1778
+ if (target) {
1779
+ const { adapter, operation } = target;
1780
+ const opBinding = adapter.operationMap[operation];
1781
+ if (
1782
+ opBinding &&
1783
+ opBinding.subjectRequirements !== undefined
1784
+ ) {
1785
+ const validatedRequirements: UdlObjectField[] = [];
1786
+ for (const req of opBinding.subjectRequirements) {
1787
+ const result = udlObjectFieldSchema.safeParse(req);
1788
+ if (!result.success || result.data.optional) {
1789
+ fail(
1790
+ entry,
1791
+ `adapter requirement ${req.name} is invalid or optional: ${result.success ? "requirements cannot be optional" : result.error.message}`,
1792
+ "ensure adapter subject requirements conform to UDL schema",
1793
+ );
1794
+ }
1795
+ validatedRequirements.push(result.data);
1796
+ }
1797
+ const declaration = {
1798
+ provider: adapter.provider,
1799
+ capability: adapter.capability,
1800
+ operation,
1801
+ requirements: validatedRequirements,
1802
+ };
1803
+ const digest = sha256(
1804
+ new TextEncoder().encode(canonicalJson(declaration)),
1805
+ );
1806
+ const snapshot: UdlAdapterSubjectSnapshot = {
1807
+ ...declaration,
1808
+ declarationDigest: Array.from(digest, (byte) =>
1809
+ byte.toString(16).padStart(2, "0"),
1810
+ ).join(""),
1811
+ };
1812
+ const adapterRenames: Record<string, string> = {};
1813
+ if (attachmentInfo?.renames) {
1814
+ for (const req of snapshot.requirements) {
1815
+ if (attachmentInfo.renames.has(req.name)) {
1816
+ adapterRenames[req.name] = attachmentInfo.renames.get(
1817
+ req.name,
1818
+ )!;
1819
+ }
1820
+ }
1821
+ }
1822
+ adapterList.push({
1823
+ binding: bindingName,
1824
+ snapshot,
1825
+ ...(Object.keys(adapterRenames).length > 0
1826
+ ? { renames: adapterRenames }
1827
+ : {}),
1828
+ });
1829
+ for (const reqField of snapshot.requirements) {
1830
+ const objectField = attachmentInfo?.renames.get(
1831
+ reqField.name,
1832
+ );
1833
+ const targetName = objectField ?? reqField.name;
1834
+ const existing = directRequirements.find(
1835
+ (r) => (r.objectField ?? r.field.name) === targetName,
1836
+ );
1837
+ if (existing) {
1838
+ if (!sameObjectField(existing.field, reqField)) {
1839
+ failWithCode(
1840
+ entry,
1841
+ "subject_field_conflict",
1842
+ `conflicting requirement ${reqField.name} in action ${name}`,
1843
+ "rename or unify the requirement",
1844
+ );
1845
+ }
1846
+ } else {
1847
+ directRequirements.push({
1848
+ field: { ...reqField },
1849
+ ...(objectField ? { objectField } : {}),
1850
+ });
1851
+ }
1852
+ }
1853
+ } else {
1854
+ adapterList.push({
1855
+ binding: bindingName,
1856
+ snapshot: null,
1857
+ });
1858
+ }
1859
+ } else {
1860
+ adapterList.push({
1861
+ binding: bindingName,
1862
+ snapshot: null,
1863
+ });
1864
+ }
1865
+ }
1866
+ } else {
1867
+ const fieldDef = lowerObjectField(entry, resolve);
1868
+ if (fieldDef.optional) {
1869
+ fail(
1870
+ entry,
1871
+ `subject requirement ${entry.key} cannot be optional`,
1872
+ "remove ? from requirement",
1873
+ );
1874
+ }
1875
+ const objectField = attachmentInfo?.renames.get(entry.key);
1876
+ const targetName = objectField ?? entry.key;
1877
+ const existing = directRequirements.find(
1878
+ (r) => (r.objectField ?? r.field.name) === targetName,
1879
+ );
1880
+ if (existing) {
1881
+ if (!sameObjectField(existing.field, fieldDef)) {
1882
+ failWithCode(
1883
+ entry,
1884
+ "subject_field_conflict",
1885
+ `conflicting requirement ${entry.key} in action ${name}`,
1886
+ "rename or unify the requirement",
1887
+ );
1888
+ }
1889
+ } else {
1890
+ directRequirements.push({
1891
+ field: fieldDef,
1892
+ ...(objectField ? { objectField } : {}),
1893
+ });
1894
+ }
1895
+ }
1896
+ }
1897
+ if (directRequirements.length > 0 || adapterList.length > 0) {
1898
+ actionSubject = {
1899
+ requirements: directRequirements,
1900
+ adapters: adapterList,
1901
+ };
1902
+ }
1903
+ }
928
1904
  const a: UdlAction = {
929
1905
  summary: slots.has("summary")
930
1906
  ? String(data(slots.get("summary")!))
@@ -935,7 +1911,20 @@ export function compile(
935
1911
  input: lowerFields(asBlock(slots.get("input"))),
936
1912
  requires: [],
937
1913
  moves: [],
1914
+ ...(actionSubject ? { subject: actionSubject } : {}),
938
1915
  };
1916
+ for (const binding of boundaryBindings)
1917
+ if (
1918
+ !a.subject?.adapters.find((entry) => entry.binding === binding)
1919
+ ?.snapshot
1920
+ )
1921
+ fail(
1922
+ row,
1923
+ `boundary adapter ${binding} has no declared requirements`,
1924
+ "declare the adapter subject requirements, including an explicit empty list",
1925
+ );
1926
+ currentAction = a;
1927
+ currentActionName = name;
939
1928
  if (name !== "create") {
940
1929
  const from = slots.get("from");
941
1930
  const to = slots.get("to");
@@ -953,7 +1942,8 @@ export function compile(
953
1942
  };
954
1943
  }
955
1944
  for (const [key, expr] of slots) {
956
- if (["from", "to", "input", "summary"].includes(key)) continue;
1945
+ if (["from", "to", "input", "summary", "subject"].includes(key))
1946
+ continue;
957
1947
  if (key === "moves") {
958
1948
  const moves = expr.kind === "list" ? expr.items : [expr];
959
1949
  for (const [index, move] of moves.entries()) {
@@ -987,6 +1977,25 @@ export function compile(
987
1977
  const amount = parts.get("amount"),
988
1978
  from = parts.get("from"),
989
1979
  to = parts.get("to");
1980
+ if (amount) {
1981
+ const resolvedAmount = resolve(amount);
1982
+ if (
1983
+ resolvedAmount.kind === "name" &&
1984
+ resolvedAmount.value.startsWith("subject.")
1985
+ ) {
1986
+ const subField = resolvedAmount.value.split(".")[1]!;
1987
+ const req = a.subject?.requirements.find(
1988
+ (r) => r.field.name === subField,
1989
+ );
1990
+ if (req && req.field.type !== "money") {
1991
+ fail(
1992
+ amount,
1993
+ `move amount subject.${subField} must have type money`,
1994
+ "use a money field",
1995
+ );
1996
+ }
1997
+ }
1998
+ }
990
1999
  if (parts.has("shares")) {
991
2000
  if (
992
2001
  !amount ||
@@ -1018,7 +2027,7 @@ export function compile(
1018
2027
  });
1019
2028
  if (
1020
2029
  recipient.kind !== "name" ||
1021
- !document.parties[recipient.value] ||
2030
+ !isParty(recipient.value) ||
1022
2031
  rate.kind !== "percent"
1023
2032
  )
1024
2033
  fail(
@@ -1093,6 +2102,19 @@ export function compile(
1093
2102
  ...transfer,
1094
2103
  operation: op,
1095
2104
  capture: String(data(parts.get("capture")!)),
2105
+ ...(parts.has("boundary")
2106
+ ? {
2107
+ boundary: {
2108
+ adapter: text(
2109
+ resolve(
2110
+ entries(
2111
+ asBlock(parts.get("boundary")),
2112
+ ).get("adapter")!,
2113
+ ),
2114
+ ),
2115
+ },
2116
+ }
2117
+ : {}),
1096
2118
  }
1097
2119
  : { ...transfer, operation: op },
1098
2120
  );
@@ -1252,7 +2274,41 @@ export function compile(
1252
2274
  a.approval = approval;
1253
2275
  } else (a as unknown as Record<string, unknown>)[key] = data(expr);
1254
2276
  }
2277
+ if (attachmentInfo) {
2278
+ if (attachmentInfo.exposed.has(name)) {
2279
+ a.publicAction = attachmentInfo.exposed.get(name)!;
2280
+ } else {
2281
+ delete a.publicAction;
2282
+ }
2283
+ }
1255
2284
  if (automatic(a.actor)) delete a.publicAction;
2285
+ const checkSubjectPaths = (obj: unknown, span: Span) => {
2286
+ if (typeof obj === "string") {
2287
+ if (obj.startsWith("subject.")) {
2288
+ const subField = obj.split(".")[1]!;
2289
+ const req = a.subject?.requirements.find(
2290
+ (r) => r.field.name === subField,
2291
+ );
2292
+ if (!req) {
2293
+ failWithCode(
2294
+ { span },
2295
+ "subject_field_unknown",
2296
+ `subject.${subField} names no declared subject requirement in action ${name}`,
2297
+ `declare ${subField} in subject { ... }`,
2298
+ );
2299
+ }
2300
+ }
2301
+ } else if (Array.isArray(obj)) {
2302
+ for (const item of obj) checkSubjectPaths(item, span);
2303
+ } else if (obj !== null && typeof obj === "object") {
2304
+ for (const val of Object.values(obj)) checkSubjectPaths(val, span);
2305
+ }
2306
+ };
2307
+ checkSubjectPaths(a.requires, row.span);
2308
+ checkSubjectPaths(a.set, row.span);
2309
+ checkSubjectPaths(a.invoke, row.span);
2310
+ currentAction = undefined;
2311
+ currentActionName = undefined;
1256
2312
  for (const requirement of a.requires ?? []) {
1257
2313
  if (
1258
2314
  requirement.kind !== "approval" ||
@@ -1262,23 +2318,43 @@ export function compile(
1262
2318
  const action = requirement.action ?? name;
1263
2319
  const key = `${id}_${action}_decision`;
1264
2320
  const previous = implicitDecisions.get(key);
1265
- if (previous && previous.party !== requirement.party)
2321
+ if (
2322
+ previous &&
2323
+ (previous.party !== requirement.party ||
2324
+ previous.protectedRequest !==
2325
+ (requirement.protectedRequest ?? "self"))
2326
+ )
1266
2327
  fail(
1267
2328
  { span: origin },
1268
- `action ${action} has multiple approval parties`,
2329
+ `action ${action} has conflicting approval parties or protected requests`,
1269
2330
  "use a separate decision action for each party",
1270
2331
  );
1271
2332
  implicitDecisions.set(key, {
1272
2333
  target: id,
1273
2334
  action,
1274
2335
  party: requirement.party,
2336
+ protectedRequest: requirement.protectedRequest ?? "self",
1275
2337
  origin,
1276
2338
  });
1277
2339
  }
2340
+ for (const requirement of a.subject?.requirements ?? []) {
2341
+ const entry =
2342
+ asBlock(subjectExpr).entries.find(
2343
+ (entry) => entry.key === requirement.field.name,
2344
+ ) ??
2345
+ asBlock(subjectExpr).entries.find(
2346
+ (entry) => entry.key === "adapter",
2347
+ );
2348
+ requirementOrigins.set(requirement, {
2349
+ source: declarationSources.get(decl) ?? "program",
2350
+ span: entry?.span ?? row.span,
2351
+ message: `${id}.${name}.subject.${requirement.field.name}`,
2352
+ });
2353
+ }
1278
2354
  inst.actions[name] = a;
1279
2355
  inst.actionOrder.push(name);
1280
2356
  }
1281
- for (const key of ["invariants"] as const)
2357
+ for (const key of ["invariants", "reports", "revisioned"] as const)
1282
2358
  if (body.has(key))
1283
2359
  (inst as unknown as Record<string, unknown>)[key] = data(
1284
2360
  body.get(key)!,
@@ -1287,6 +2363,13 @@ export function compile(
1287
2363
  path: `$.instruments[${document.instruments.length}]`,
1288
2364
  span: { ...origin, ...lineColAt(source, origin.start) },
1289
2365
  });
2366
+ if (familyDeclaration && familyDeclaration.revision !== undefined) {
2367
+ inst.family = {
2368
+ module: familyDeclaration.module,
2369
+ exportPath: familyDeclaration.exportPath,
2370
+ revision: familyDeclaration.revision,
2371
+ };
2372
+ }
1290
2373
  document.instruments.push(inst);
1291
2374
  for (const [key, definition] of records) {
1292
2375
  const child: InstrumentDecl = {
@@ -1296,6 +2379,19 @@ export function compile(
1296
2379
  body: asBlock(definition),
1297
2380
  span: origin,
1298
2381
  };
2382
+ declarationSources.set(
2383
+ child,
2384
+ declarationSources.get(decl) ?? "program",
2385
+ );
2386
+ const childFamily = familyDeclaration
2387
+ ? {
2388
+ module: familyDeclaration.module,
2389
+ exportPath: `${familyDeclaration.exportPath}.${key}`,
2390
+ ...(familyDeclaration.revision !== undefined
2391
+ ? { revision: familyDeclaration.revision }
2392
+ : {}),
2393
+ }
2394
+ : undefined;
1299
2395
  addInstrument(
1300
2396
  child,
1301
2397
  `${id}_${key}`,
@@ -1307,8 +2403,220 @@ export function compile(
1307
2403
  ]),
1308
2404
  approvers,
1309
2405
  enums,
2406
+ attachmentInfo
2407
+ ? { ...attachmentInfo, exposed: new Map() }
2408
+ : undefined,
2409
+ childFamily,
2410
+ );
2411
+ }
2412
+ };
2413
+ const compileObject = (decl: ObjectDecl) => {
2414
+ const body = entries(decl.body);
2415
+ for (const key of body.keys()) {
2416
+ if (
2417
+ key !== "fields" &&
2418
+ key !== "columns" &&
2419
+ !key.startsWith("attach ")
2420
+ ) {
2421
+ fail(
2422
+ decl,
2423
+ `unknown object clause ${key}`,
2424
+ "use fields, columns, or attach",
2425
+ );
2426
+ }
2427
+ }
2428
+
2429
+ // 1. Lower authored fields
2430
+ const authoredFields: UdlObjectField[] = [];
2431
+ const authoredNames: string[] = [];
2432
+ const fieldsBlock = asBlock(body.get("fields"));
2433
+ for (const row of fieldsBlock.entries) {
2434
+ if (RESERVED_OBJECT_NAMES.some((name) => name === row.key)) {
2435
+ fail(
2436
+ row,
2437
+ `${row.key} is a reserved object name`,
2438
+ "rename this field",
2439
+ );
2440
+ }
2441
+ const fieldDef = lowerObjectField(row);
2442
+ authoredFields.push(fieldDef);
2443
+ authoredNames.push(row.key);
2444
+ }
2445
+
2446
+ // 2. Process attachments
2447
+ const attachments: UdlObjectAttachment[] = [];
2448
+ for (const entry of decl.body.entries) {
2449
+ if (!entry.key.startsWith("attach ")) continue;
2450
+ const match = /^attach\s+([A-Za-z0-9_]+)\s*=\s*(.+)$/.exec(entry.key);
2451
+ if (!match) {
2452
+ fail(
2453
+ entry,
2454
+ "invalid attach syntax",
2455
+ "write attach name = template { ... }",
2456
+ );
2457
+ }
2458
+ const attachmentName = match[1]!;
2459
+ const targetTemplate = match[2]!;
2460
+ const template = templates.get(targetTemplate);
2461
+ if (!template) {
2462
+ fail(
2463
+ entry,
2464
+ `unknown instrument ${targetTemplate}`,
2465
+ `add use ${targetTemplate.split(".")[0]} and choose a declared instrument`,
2466
+ );
2467
+ }
2468
+ const instId = `${decl.name}_${attachmentName}`;
2469
+
2470
+ const attachmentBlock = asBlock(entry.value);
2471
+ const renames = new Map<string, string>();
2472
+ const renameEntries = new Map<string, Entry>();
2473
+ const exposed = new Map<string, string>();
2474
+ const tunableEntries: Entry[] = [];
2475
+
2476
+ for (const row of attachmentBlock.entries) {
2477
+ if (row.key === "rename") {
2478
+ for (const r of asBlock(row.value).entries) {
2479
+ renames.set(r.key, text(r.value));
2480
+ renameEntries.set(r.key, r);
2481
+ }
2482
+ } else if (row.key === "expose") {
2483
+ if (row.value.kind === "call") {
2484
+ const actionName = row.value.name;
2485
+ const publicName = text(row.value.args[0]!);
2486
+ exposed.set(actionName, publicName);
2487
+ }
2488
+ } else {
2489
+ tunableEntries.push(row);
2490
+ }
2491
+ }
2492
+
2493
+ const parties: Record<string, AttachmentPartyBinding> = {};
2494
+ attachments.push({ name: attachmentName, instrument: instId, parties });
2495
+
2496
+ const tunableBlock: BlockExpr = {
2497
+ kind: "block",
2498
+ entries: tunableEntries,
2499
+ span: entry.value.span,
2500
+ };
2501
+
2502
+ const templateFamily = resolveFamily(targetTemplate, entry, false);
2503
+ addInstrument(
2504
+ template,
2505
+ instId,
2506
+ tunableBlock,
2507
+ entry.span,
2508
+ new Map(),
2509
+ new Set(),
2510
+ new Map(),
2511
+ {
2512
+ subjectKindId: decl.name,
2513
+ attachmentName,
2514
+ renames,
2515
+ exposed,
2516
+ parties,
2517
+ },
2518
+ templateFamily ? { ...templateFamily } : undefined,
1310
2519
  );
2520
+
2521
+ const attachedInst = document.instruments.find((i) => i.id === instId);
2522
+ if (attachedInst?.actions.create) {
2523
+ const owned = new Set(
2524
+ attachedInst.calculate.map((node) => node.target),
2525
+ );
2526
+ for (const action of Object.values(attachedInst.actions)) {
2527
+ for (const node of action.calculate ?? []) owned.add(node.target);
2528
+ for (const move of action.moves)
2529
+ if ("capture" in move && move.capture) owned.add(move.capture);
2530
+ }
2531
+ const create = attachedInst.actions.create;
2532
+ for (const field of attachedInst.fields) {
2533
+ if (
2534
+ field.type === "account" ||
2535
+ (field.type === "ref" && field.targetKind === "instrument") ||
2536
+ (field.type === "list" &&
2537
+ field.item === "ref" &&
2538
+ field.targetKind === "instrument") ||
2539
+ field.optional ||
2540
+ "value" in field ||
2541
+ owned.has(field.name)
2542
+ )
2543
+ continue;
2544
+ create.subject ??= { requirements: [], adapters: [] };
2545
+ const existing = create.subject.requirements.find(
2546
+ (item) => item.field.name === field.name,
2547
+ );
2548
+ if (existing) {
2549
+ if (canonicalJson(existing.field) !== canonicalJson(field))
2550
+ failWithCode(
2551
+ entry,
2552
+ "subject_field_conflict",
2553
+ `${instId}.create.subject.${field.name} conflicts with its instrument field`,
2554
+ "use the instrument field's type and constraints",
2555
+ );
2556
+ continue;
2557
+ }
2558
+ const objectField = renames.get(field.name);
2559
+ const requirement = {
2560
+ field,
2561
+ ...(objectField ? { objectField } : {}),
2562
+ };
2563
+ create.subject.requirements.push(requirement);
2564
+ requirementOrigins.set(requirement, {
2565
+ source: declarationSources.get(template) ?? "program",
2566
+ span: entry.span,
2567
+ message: `${instId}.create.fields.${field.name}`,
2568
+ });
2569
+ }
2570
+ }
2571
+ for (const [oldName] of renames) {
2572
+ const found =
2573
+ attachedInst &&
2574
+ Object.values(attachedInst.actions).some((action) =>
2575
+ action.subject?.requirements.some(
2576
+ (requirement) => requirement.field.name === oldName,
2577
+ ),
2578
+ );
2579
+ if (!found) {
2580
+ const renameEntry = renameEntries.get(oldName) ?? entry;
2581
+ failWithCode(
2582
+ renameEntry,
2583
+ "subject_field_unknown",
2584
+ `rename source '${oldName}' is not a declared subject requirement of ${targetTemplate}`,
2585
+ "rename a declared subject requirement",
2586
+ );
2587
+ }
2588
+ }
1311
2589
  }
2590
+
2591
+ // 4. Validate columns
2592
+ const columnsExpr = body.get("columns");
2593
+ let columns: string[] = [];
2594
+ if (columnsExpr) {
2595
+ if (columnsExpr.kind !== "list") {
2596
+ fail(
2597
+ columnsExpr,
2598
+ "columns needs a list of field names",
2599
+ "write columns: [name, ...]",
2600
+ );
2601
+ }
2602
+ columns = columnsExpr.items.map(text);
2603
+ if (columns.length > 8) {
2604
+ fail(
2605
+ columnsExpr,
2606
+ "at most 8 columns allowed",
2607
+ "choose up to 8 columns",
2608
+ );
2609
+ }
2610
+ }
2611
+
2612
+ document.objects.push({
2613
+ id: decl.name,
2614
+ title: decl.title,
2615
+ authoredFields: authoredNames,
2616
+ attachments,
2617
+ fields: authoredFields,
2618
+ columns,
2619
+ });
1312
2620
  };
1313
2621
  for (const decl of program.decls) {
1314
2622
  if (decl.kind === "instrument") {
@@ -1320,15 +2628,172 @@ export function compile(
1320
2628
  );
1321
2629
  addInstrument(decl, decl.name, emptyBlock, decl.span);
1322
2630
  }
1323
- if (decl.kind === "object") {
1324
- const template = templates.get(decl.object);
2631
+ if (decl.kind === "assignment") {
2632
+ const template = templates.get(decl.target);
1325
2633
  if (!template)
1326
2634
  fail(
1327
2635
  decl,
1328
- `unknown object ${decl.object}`,
1329
- `add use ${decl.object.split(".")[0]} and choose a declared object`,
2636
+ `unknown instrument ${decl.target}`,
2637
+ `add use ${decl.target.split(".")[0]} and choose a declared instrument`,
2638
+ );
2639
+ const templateFamily = resolveFamily(decl.target, decl, false);
2640
+ addInstrument(
2641
+ template,
2642
+ decl.name,
2643
+ decl.body,
2644
+ decl.span,
2645
+ new Map(),
2646
+ new Set(),
2647
+ new Map(),
2648
+ undefined,
2649
+ templateFamily ? { ...templateFamily } : undefined,
2650
+ );
2651
+ }
2652
+ if (decl.kind === "object") {
2653
+ compileObject(decl);
2654
+ }
2655
+ }
2656
+ // Propagate mandatory invoked action requirements
2657
+ let changedInvocations = true;
2658
+ let invocationIterations = 0;
2659
+ while (changedInvocations && invocationIterations < 32) {
2660
+ changedInvocations = false;
2661
+ invocationIterations++;
2662
+ for (const inst of document.instruments) {
2663
+ for (const action of Object.values(inst.actions)) {
2664
+ for (const call of action.invoke ?? []) {
2665
+ if (call.guard) continue;
2666
+ let targetIds: string[] = [];
2667
+ if ("instrument" in call) {
2668
+ targetIds = [call.instrument];
2669
+ } else if ("selection" in call) {
2670
+ targetIds = Array.isArray(call.selection.instrument)
2671
+ ? call.selection.instrument
2672
+ : [call.selection.instrument];
2673
+ } else if ("reference" in call) {
2674
+ const refField = resolveField(
2675
+ document,
2676
+ inst,
2677
+ call.reference,
2678
+ action.input,
2679
+ action,
2680
+ );
2681
+ if (
2682
+ refField?.type === "ref" &&
2683
+ refField.targetKind === "instrument"
2684
+ ) {
2685
+ targetIds = Array.isArray(refField.target)
2686
+ ? refField.target
2687
+ : [refField.target];
2688
+ }
2689
+ }
2690
+ for (const targetId of targetIds) {
2691
+ const targetInst = document.instruments.find(
2692
+ (i) => i.id === targetId,
2693
+ );
2694
+ if (!targetInst) continue;
2695
+ if (
2696
+ inst.subject &&
2697
+ targetInst.subject &&
2698
+ inst.subject === targetInst.subject
2699
+ ) {
2700
+ const targetAction = targetInst.actions[call.action];
2701
+ if (!targetAction?.subject) continue;
2702
+ if (!action.subject) {
2703
+ action.subject = { requirements: [], adapters: [] };
2704
+ }
2705
+ for (const adapter of targetAction.subject.adapters) {
2706
+ if (
2707
+ !action.subject.adapters.some(
2708
+ (existing) => existing.binding === adapter.binding,
2709
+ )
2710
+ ) {
2711
+ action.subject.adapters.push(adapter);
2712
+ changedInvocations = true;
2713
+ }
2714
+ }
2715
+ for (const targetReq of targetAction.subject.requirements) {
2716
+ const targetObjFieldName =
2717
+ targetReq.objectField ?? targetReq.field.name;
2718
+ const existing = action.subject.requirements.find(
2719
+ (cr) =>
2720
+ (cr.objectField ?? cr.field.name) === targetObjFieldName,
2721
+ );
2722
+ if (!existing) {
2723
+ const inherited = {
2724
+ field: { ...targetReq.field, name: targetObjFieldName },
2725
+ };
2726
+ action.subject.requirements.push(inherited);
2727
+ requirementOrigins.set(
2728
+ inherited,
2729
+ requirementOrigins.get(targetReq)!,
2730
+ );
2731
+ changedInvocations = true;
2732
+ } else if (
2733
+ !sameObjectField(existing.field, targetReq.field)
2734
+ ) {
2735
+ const first = requirementOrigins.get(existing)!;
2736
+ const second = requirementOrigins.get(targetReq)!;
2737
+ throw new CompileFailure({
2738
+ code: "subject_field_conflict",
2739
+ message: `${first.message} conflicts with ${second.message}: incompatible requirement '${targetObjFieldName}'`,
2740
+ fix: "ensure compatible requirement definitions across invoked actions",
2741
+ span: first.span,
2742
+ source: first.source,
2743
+ related: [first, second],
2744
+ });
2745
+ }
2746
+ }
2747
+ }
2748
+ }
2749
+ }
2750
+ }
2751
+ }
2752
+ }
2753
+ for (const kind of document.objects) {
2754
+ const origins = new Map(
2755
+ kind.fields.map((field) => [
2756
+ field.name,
2757
+ `authored field ${kind.id}.${field.name}`,
2758
+ ]),
2759
+ );
2760
+ for (const instrument of document.instruments.filter(
2761
+ (item) => item.subject === kind.id,
2762
+ )) {
2763
+ for (const name of instrument.actionOrder) {
2764
+ for (const requirement of instrument.actions[name]!.subject
2765
+ ?.requirements ?? []) {
2766
+ const field = {
2767
+ ...requirement.field,
2768
+ name: requirement.objectField ?? requirement.field.name,
2769
+ };
2770
+ const previous = kind.fields.find(
2771
+ (item) => item.name === field.name,
2772
+ );
2773
+ const origin = `${instrument.id}.${name}.subject.${requirement.field.name}`;
2774
+ if (previous && !sameObjectField(previous, field)) {
2775
+ failWithCode(
2776
+ objects.get(kind.id)!,
2777
+ "subject_field_conflict",
2778
+ `${origin} conflicts with ${origins.get(field.name)}: incompatible types or constraints`,
2779
+ "rename fields with different meanings",
2780
+ );
2781
+ }
2782
+ if (!previous) {
2783
+ kind.fields.push(field);
2784
+ origins.set(field.name, origin);
2785
+ }
2786
+ }
2787
+ }
2788
+ }
2789
+ for (const column of kind.columns) {
2790
+ if (!kind.fields.some((field) => field.name === column))
2791
+ failWithCode(
2792
+ objects.get(kind.id)!,
2793
+ "subject_field_unknown",
2794
+ `column ${column} is unknown on ${kind.id}`,
2795
+ "name an authored field or attached requirement",
1330
2796
  );
1331
- addInstrument(template, decl.name, decl.body, decl.span);
1332
2797
  }
1333
2798
  }
1334
2799
  const eliminatedStates = new Map<string, Set<string>>();
@@ -1361,7 +2826,7 @@ export function compile(
1361
2826
  for (let n = 0; n < inst.lifecycle.states.length; n++)
1362
2827
  for (const edge of Object.values(inst.lifecycle.transitions))
1363
2828
  if (edge.from.some((state) => reachable.has(state)))
1364
- reachable.add(edge.to);
2829
+ if (edge.to !== "preserve") reachable.add(edge.to);
1365
2830
  for (const [key, edge] of Object.entries(inst.lifecycle.transitions)) {
1366
2831
  edge.from = edge.from.filter((state) => reachable.has(state));
1367
2832
  if (!edge.from.length) {
@@ -1426,12 +2891,33 @@ export function compile(
1426
2891
  (action) =>
1427
2892
  action.approval?.action === decision.action &&
1428
2893
  action.approval.party === decision.party &&
1429
- inst.fields.some(
1430
- (field) =>
1431
- field.type === "ref" &&
1432
- field.target === decision.target &&
1433
- `self.${field.name}` === action.approval?.target,
1434
- ),
2894
+ inst.fields.some((field) => {
2895
+ if (
2896
+ field.type !== "ref" ||
2897
+ field.target !== decision.target ||
2898
+ `self.${field.name}` !== action.approval?.target
2899
+ )
2900
+ return false;
2901
+ const [root, name, ...tail] =
2902
+ decision.protectedRequest.split(".");
2903
+ const input = name && action.approval.input[name];
2904
+ const request =
2905
+ root === "self"
2906
+ ? [action.approval.target, name, ...tail]
2907
+ .filter(Boolean)
2908
+ .join(".")
2909
+ : root === "input" && name
2910
+ ? input && "field" in input
2911
+ ? [input.field, ...tail].join(".")
2912
+ : materialApprovals.has(action)
2913
+ ? [`self.material_${name}`, ...tail].join(".")
2914
+ : undefined
2915
+ : undefined;
2916
+ return (
2917
+ request !== undefined &&
2918
+ action.approval.protectedRequest === request
2919
+ );
2920
+ }),
1435
2921
  ),
1436
2922
  );
1437
2923
  if (existing) continue;
@@ -1441,6 +2927,16 @@ export function compile(
1441
2927
  `implicit decision name ${id} is already used`,
1442
2928
  "rename the conflicting object",
1443
2929
  );
2930
+ const protectedRequest =
2931
+ decision.protectedRequest === "self"
2932
+ ? "self.target"
2933
+ : decision.protectedRequest.startsWith("self.")
2934
+ ? `self.target.${decision.protectedRequest.slice(5)}`
2935
+ : fail(
2936
+ { span: decision.origin },
2937
+ "implicit approval needs a stored protected request",
2938
+ "declare an explicit decision for an input-based protected request",
2939
+ );
1444
2940
  addInstrument(
1445
2941
  template,
1446
2942
  id,
@@ -1451,10 +2947,14 @@ export function compile(
1451
2947
  for: decision.target,
1452
2948
  approved_by: decision.party,
1453
2949
  action: decision.action,
2950
+ protected_request: protectedRequest,
1454
2951
  }).map(([key, value]) => ({
1455
2952
  key,
1456
2953
  value: {
1457
- kind: key === "action" ? "text" : "name",
2954
+ kind:
2955
+ key === "action" || key === "protected_request"
2956
+ ? "text"
2957
+ : "name",
1458
2958
  value,
1459
2959
  span: decision.origin,
1460
2960
  },
@@ -1462,6 +2962,22 @@ export function compile(
1462
2962
  })),
1463
2963
  },
1464
2964
  decision.origin,
2965
+ new Map(),
2966
+ new Set(),
2967
+ new Map(),
2968
+ document.instruments.find(
2969
+ (instrument) => instrument.id === decision.target,
2970
+ )?.subject
2971
+ ? {
2972
+ subjectKindId: document.instruments.find(
2973
+ (instrument) => instrument.id === decision.target,
2974
+ )!.subject!,
2975
+ attachmentName: id,
2976
+ parties: {},
2977
+ renames: new Map(),
2978
+ exposed: new Map(),
2979
+ }
2980
+ : undefined,
1465
2981
  );
1466
2982
  }
1467
2983
  }
@@ -1532,7 +3048,7 @@ export function compile(
1532
3048
  .find((o) => i.path.startsWith(o.path));
1533
3049
  return diagnostic(
1534
3050
  {
1535
- code: "HSX1601",
3051
+ code: i.code.startsWith("UDL") ? "HSX1601" : i.code,
1536
3052
  message: `${i.path}: ${i.message}`,
1537
3053
  fix: i.fix,
1538
3054
  span: origin?.span ?? program.span,