@hyperscale0/hsx 3.3.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 (51) hide show
  1. package/CHANGELOG.md +4 -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 +1162 -101
  10. package/dist/src/compile.js.map +1 -1
  11. package/dist/src/headers.d.ts +2 -2
  12. package/dist/src/headers.d.ts.map +1 -1
  13. package/dist/src/headers.js +2 -3
  14. package/dist/src/headers.js.map +1 -1
  15. package/dist/src/index.d.ts +3 -2
  16. package/dist/src/index.d.ts.map +1 -1
  17. package/dist/src/index.js.map +1 -1
  18. package/dist/src/lex.d.ts +1 -1
  19. package/dist/src/lex.d.ts.map +1 -1
  20. package/dist/src/lex.js +5 -0
  21. package/dist/src/lex.js.map +1 -1
  22. package/dist/src/parse.js +87 -3
  23. package/dist/src/parse.js.map +1 -1
  24. package/dist/src/std-bundle.d.ts.map +1 -1
  25. package/dist/src/std-bundle.js +8 -9
  26. package/dist/src/std-bundle.js.map +1 -1
  27. package/dist/src/version.d.ts +2 -2
  28. package/dist/src/version.js +2 -2
  29. package/docs/README.md +45 -27
  30. package/docs/headers.md +43 -44
  31. package/examples/cost-table.json +8 -324
  32. package/examples/library.hsx +12 -64
  33. package/package.json +7 -5
  34. package/src/ast.ts +11 -1
  35. package/src/cli.ts +1 -1
  36. package/src/compile.ts +1588 -118
  37. package/src/headers.ts +2 -3
  38. package/src/index.ts +12 -1
  39. package/src/lex.ts +5 -0
  40. package/src/parse.ts +84 -3
  41. package/src/std-bundle.ts +8 -9
  42. package/src/version.ts +2 -2
  43. package/std/approvals.hsx +3 -3
  44. package/std/escrow.hsx +16 -14
  45. package/std/financing.hsx +59 -17
  46. package/std/insurance.hsx +4 -5
  47. package/std/lending.hsx +7 -8
  48. package/std/marketplace.hsx +2 -5
  49. package/std/money.hsx +15 -49
  50. package/std/travel.hsx +5 -6
  51. package/std/vehicles.hsx +0 -34
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,6 +1158,7 @@ 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",
@@ -606,6 +1175,24 @@ export function compile(
606
1175
  `unknown instrument clause ${key}`,
607
1176
  "use fields, lifecycle, actions, invariants, or records",
608
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
+ }
609
1196
  const records = entries(asBlock(body.get("records")));
610
1197
  if (!inherited.size)
611
1198
  environment.set("parent", { kind: "name", value: id, span: origin });
@@ -618,11 +1205,63 @@ export function compile(
618
1205
  });
619
1206
  const fields: UdlField[] = [];
620
1207
  const calculations: UdlCalculation[] = [];
1208
+ let currentAction: UdlAction | undefined;
1209
+ let currentActionName: string | undefined;
621
1210
  const path = (expr: Expr): string => {
622
1211
  const value = resolve(expr);
623
1212
  const name = text(value);
624
- if (document.parties[name]) return `party.${name}`;
625
- 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}`;
626
1265
  };
627
1266
  const val = (expr: Expr): UdlValue => {
628
1267
  const v = resolve(expr);
@@ -639,8 +1278,57 @@ export function compile(
639
1278
  return id;
640
1279
  const value = resolve(expr);
641
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
+
642
1330
  const result = Object.fromEntries(
643
- [...entries(value)].map(([key, value]) => [key, data(value)]),
1331
+ [...rawEntries].map(([key, value]) => [key, data(value)]),
644
1332
  );
645
1333
  const selection = result.selection as
646
1334
  | { instrument?: unknown }
@@ -692,66 +1380,86 @@ export function compile(
692
1380
  const t = row.value.kind === "default" ? row.value.type : row.value;
693
1381
  const constant =
694
1382
  row.value.kind === "default" ? resolve(row.value.value) : undefined;
695
- const type =
696
- t.kind === "type" || t.kind === "call" ? t.name : text(t);
697
- const f: Record<string, unknown> = {
698
- name: row.key,
699
- type,
700
- ...(t.kind === "type" && t.optional ? { optional: true } : {}),
701
- };
702
- if (type === "enum" && t.kind === "call") f.values = t.args.map(text);
703
- if (type === "text" && t.kind === "call") {
704
- if (t.args.length < 2 || t.args.length > 3)
705
- fail(
706
- t,
707
- "bounded text needs length bounds and an optional pattern",
708
- "write text(1, 80)",
709
- );
710
- f.minLength = literal(resolve(t.args[0]!));
711
- f.maxLength = literal(resolve(t.args[1]!));
712
- if (t.args[2]) f.pattern = literal(resolve(t.args[2]));
713
- }
714
- if (["integer", "money"].includes(type) && t.kind === "call") {
715
- if (t.args.length !== 2)
716
- fail(
717
- t,
718
- "bounded fields need a minimum and maximum",
719
- "write integer(1, 12) or money(0 SAR, 100 SAR)",
720
- );
721
- f.minimum = literal(resolve(t.args[0]!));
722
- f.maximum = literal(resolve(t.args[1]!));
723
- }
1383
+ const f = lowerFieldShape(row, resolve);
1384
+ const type = f.type;
724
1385
  if (type === "list" && t.kind === "call") {
725
- f.item = text(t.args[0]!);
726
- f.maxItems = t.args[1] ? literal(resolve(t.args[1])) : 366;
727
- }
728
- if (type === "list" && t.kind === "type") {
729
- f.item = t.target;
730
- 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
+ }
731
1395
  }
732
1396
  if (type === "ref") {
733
- const target = t.kind === "type" ? t.target : undefined;
734
- if (!target)
735
- fail(row, "reference needs a target", "write ref<object>");
736
- const [root, ...tail] = target.split(".");
737
- const resolved = environment.get(root!);
738
- const resolvedTargets =
739
- resolved?.kind === "list"
740
- ? resolved.items
741
- : resolved
742
- ? [resolved]
743
- : [];
744
- const targets = resolvedTargets.map((value) =>
745
- [text(resolve(value)).replaceAll(".", "_"), ...tail].join("_"),
746
- );
747
- f.target =
748
- target === "self"
749
- ? id
750
- : targets.length === 1
751
- ? targets[0]
752
- : targets.length
753
- ? targets
754
- : 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
+ }
755
1463
  } else if (type === "account") {
756
1464
  if (t.kind === "call") {
757
1465
  if (t.args.length < 1 || t.args.length > 4)
@@ -764,7 +1472,13 @@ export function compile(
764
1472
  f.book = t.args[1] ? text(t.args[1]) : "cash";
765
1473
  if (t.args[2]) {
766
1474
  const mode = text(t.args[2]);
767
- 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;
768
1482
  else f.key = mode;
769
1483
  }
770
1484
  if (t.args[3]) f.key = text(t.args[3]);
@@ -889,6 +1603,7 @@ export function compile(
889
1603
  lifecycle.transitions = {};
890
1604
  const inst: UdlInstrument = {
891
1605
  id,
1606
+ ...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
892
1607
  title: title(id),
893
1608
  summary: body.has("summary")
894
1609
  ? String(data(body.get("summary")!))
@@ -921,9 +1636,9 @@ export function compile(
921
1636
  // appear after the instrument that asks about its fields.
922
1637
  const bound = resolve(binding!);
923
1638
  const [root, ...children] = text(bound).split(".");
924
- const object = objects.get(root!);
925
- let target = object
926
- ? templates.get(object.object)?.body
1639
+ const assignment = assignments.get(root!);
1640
+ let target = assignment
1641
+ ? templates.get(assignment.target)?.body
927
1642
  : program.decls
928
1643
  .filter(
929
1644
  (decl): decl is InstrumentDecl =>
@@ -971,6 +1686,221 @@ export function compile(
971
1686
  }),
972
1687
  });
973
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
+ }
974
1904
  const a: UdlAction = {
975
1905
  summary: slots.has("summary")
976
1906
  ? String(data(slots.get("summary")!))
@@ -981,7 +1911,20 @@ export function compile(
981
1911
  input: lowerFields(asBlock(slots.get("input"))),
982
1912
  requires: [],
983
1913
  moves: [],
1914
+ ...(actionSubject ? { subject: actionSubject } : {}),
984
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;
985
1928
  if (name !== "create") {
986
1929
  const from = slots.get("from");
987
1930
  const to = slots.get("to");
@@ -999,7 +1942,8 @@ export function compile(
999
1942
  };
1000
1943
  }
1001
1944
  for (const [key, expr] of slots) {
1002
- if (["from", "to", "input", "summary"].includes(key)) continue;
1945
+ if (["from", "to", "input", "summary", "subject"].includes(key))
1946
+ continue;
1003
1947
  if (key === "moves") {
1004
1948
  const moves = expr.kind === "list" ? expr.items : [expr];
1005
1949
  for (const [index, move] of moves.entries()) {
@@ -1033,6 +1977,25 @@ export function compile(
1033
1977
  const amount = parts.get("amount"),
1034
1978
  from = parts.get("from"),
1035
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
+ }
1036
1999
  if (parts.has("shares")) {
1037
2000
  if (
1038
2001
  !amount ||
@@ -1064,7 +2027,7 @@ export function compile(
1064
2027
  });
1065
2028
  if (
1066
2029
  recipient.kind !== "name" ||
1067
- !document.parties[recipient.value] ||
2030
+ !isParty(recipient.value) ||
1068
2031
  rate.kind !== "percent"
1069
2032
  )
1070
2033
  fail(
@@ -1139,6 +2102,19 @@ export function compile(
1139
2102
  ...transfer,
1140
2103
  operation: op,
1141
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
+ : {}),
1142
2118
  }
1143
2119
  : { ...transfer, operation: op },
1144
2120
  );
@@ -1298,7 +2274,41 @@ export function compile(
1298
2274
  a.approval = approval;
1299
2275
  } else (a as unknown as Record<string, unknown>)[key] = data(expr);
1300
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
+ }
1301
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;
1302
2312
  for (const requirement of a.requires ?? []) {
1303
2313
  if (
1304
2314
  requirement.kind !== "approval" ||
@@ -1308,19 +2318,39 @@ export function compile(
1308
2318
  const action = requirement.action ?? name;
1309
2319
  const key = `${id}_${action}_decision`;
1310
2320
  const previous = implicitDecisions.get(key);
1311
- if (previous && previous.party !== requirement.party)
2321
+ if (
2322
+ previous &&
2323
+ (previous.party !== requirement.party ||
2324
+ previous.protectedRequest !==
2325
+ (requirement.protectedRequest ?? "self"))
2326
+ )
1312
2327
  fail(
1313
2328
  { span: origin },
1314
- `action ${action} has multiple approval parties`,
2329
+ `action ${action} has conflicting approval parties or protected requests`,
1315
2330
  "use a separate decision action for each party",
1316
2331
  );
1317
2332
  implicitDecisions.set(key, {
1318
2333
  target: id,
1319
2334
  action,
1320
2335
  party: requirement.party,
2336
+ protectedRequest: requirement.protectedRequest ?? "self",
1321
2337
  origin,
1322
2338
  });
1323
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
+ }
1324
2354
  inst.actions[name] = a;
1325
2355
  inst.actionOrder.push(name);
1326
2356
  }
@@ -1333,6 +2363,13 @@ export function compile(
1333
2363
  path: `$.instruments[${document.instruments.length}]`,
1334
2364
  span: { ...origin, ...lineColAt(source, origin.start) },
1335
2365
  });
2366
+ if (familyDeclaration && familyDeclaration.revision !== undefined) {
2367
+ inst.family = {
2368
+ module: familyDeclaration.module,
2369
+ exportPath: familyDeclaration.exportPath,
2370
+ revision: familyDeclaration.revision,
2371
+ };
2372
+ }
1336
2373
  document.instruments.push(inst);
1337
2374
  for (const [key, definition] of records) {
1338
2375
  const child: InstrumentDecl = {
@@ -1342,6 +2379,19 @@ export function compile(
1342
2379
  body: asBlock(definition),
1343
2380
  span: origin,
1344
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;
1345
2395
  addInstrument(
1346
2396
  child,
1347
2397
  `${id}_${key}`,
@@ -1353,9 +2403,221 @@ export function compile(
1353
2403
  ]),
1354
2404
  approvers,
1355
2405
  enums,
2406
+ attachmentInfo
2407
+ ? { ...attachmentInfo, exposed: new Map() }
2408
+ : undefined,
2409
+ childFamily,
1356
2410
  );
1357
2411
  }
1358
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,
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
+ }
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
+ });
2620
+ };
1359
2621
  for (const decl of program.decls) {
1360
2622
  if (decl.kind === "instrument") {
1361
2623
  if (decl.parameters.length)
@@ -1366,15 +2628,172 @@ export function compile(
1366
2628
  );
1367
2629
  addInstrument(decl, decl.name, emptyBlock, decl.span);
1368
2630
  }
1369
- if (decl.kind === "object") {
1370
- const template = templates.get(decl.object);
2631
+ if (decl.kind === "assignment") {
2632
+ const template = templates.get(decl.target);
1371
2633
  if (!template)
1372
2634
  fail(
1373
2635
  decl,
1374
- `unknown object ${decl.object}`,
1375
- `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",
1376
2796
  );
1377
- addInstrument(template, decl.name, decl.body, decl.span);
1378
2797
  }
1379
2798
  }
1380
2799
  const eliminatedStates = new Map<string, Set<string>>();
@@ -1472,12 +2891,33 @@ export function compile(
1472
2891
  (action) =>
1473
2892
  action.approval?.action === decision.action &&
1474
2893
  action.approval.party === decision.party &&
1475
- inst.fields.some(
1476
- (field) =>
1477
- field.type === "ref" &&
1478
- field.target === decision.target &&
1479
- `self.${field.name}` === action.approval?.target,
1480
- ),
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
+ }),
1481
2921
  ),
1482
2922
  );
1483
2923
  if (existing) continue;
@@ -1487,6 +2927,16 @@ export function compile(
1487
2927
  `implicit decision name ${id} is already used`,
1488
2928
  "rename the conflicting object",
1489
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
+ );
1490
2940
  addInstrument(
1491
2941
  template,
1492
2942
  id,
@@ -1497,10 +2947,14 @@ export function compile(
1497
2947
  for: decision.target,
1498
2948
  approved_by: decision.party,
1499
2949
  action: decision.action,
2950
+ protected_request: protectedRequest,
1500
2951
  }).map(([key, value]) => ({
1501
2952
  key,
1502
2953
  value: {
1503
- kind: key === "action" ? "text" : "name",
2954
+ kind:
2955
+ key === "action" || key === "protected_request"
2956
+ ? "text"
2957
+ : "name",
1504
2958
  value,
1505
2959
  span: decision.origin,
1506
2960
  },
@@ -1508,6 +2962,22 @@ export function compile(
1508
2962
  })),
1509
2963
  },
1510
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,
1511
2981
  );
1512
2982
  }
1513
2983
  }
@@ -1578,7 +3048,7 @@ export function compile(
1578
3048
  .find((o) => i.path.startsWith(o.path));
1579
3049
  return diagnostic(
1580
3050
  {
1581
- code: "HSX1601",
3051
+ code: i.code.startsWith("UDL") ? "HSX1601" : i.code,
1582
3052
  message: `${i.path}: ${i.message}`,
1583
3053
  fix: i.fix,
1584
3054
  span: origin?.span ?? program.span,