@hyperscale0/hsx 3.3.0 → 4.0.1

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 +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 +1171 -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 +1607 -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,188 @@ 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 collectChildExportPaths = (
700
+ parentDecl: InstrumentDecl,
701
+ suffix: string,
702
+ ): string[] => {
703
+ const recs = entries(asBlock(entries(parentDecl.body).get("records")));
704
+ const matches: string[] = [];
705
+ for (const [recName] of recs) {
706
+ if (suffix === recName) matches.push(recName);
707
+ }
708
+ for (const [recName, recBlock] of recs) {
709
+ if (suffix.startsWith(`${recName}_`)) {
710
+ const nested = collectChildExportPaths(
711
+ {
712
+ kind: "instrument",
713
+ name: recName,
714
+ parameters: [],
715
+ body: asBlock(recBlock),
716
+ span: parentDecl.span,
717
+ },
718
+ suffix.slice(recName.length + 1),
719
+ );
720
+ for (const rest of nested) {
721
+ matches.push(`${recName}.${rest}`);
722
+ }
723
+ }
724
+ }
725
+ return matches;
726
+ };
727
+
728
+ const resolveChildExportPath = (
729
+ parentDecl: InstrumentDecl,
730
+ suffix: string,
731
+ ): string | undefined => {
732
+ const matches = collectChildExportPaths(parentDecl, suffix);
733
+ if (matches.length > 1) {
734
+ failWithCode(
735
+ parentDecl,
736
+ "HSX1001",
737
+ `ambiguous child export path suffix '${suffix}': multiple candidates (${matches.join(", ")})`,
738
+ "rename conflicting records to remove duplicate export path suffixes",
739
+ );
740
+ }
741
+ return matches[0];
742
+ };
743
+
744
+ const getInstrumentFamily = (targetId: string): UdlFamily | undefined => {
745
+ const existing = document.instruments.find((i) => i.id === targetId);
746
+ if (existing?.family) return existing.family;
747
+
748
+ if (targetId.startsWith(`${id}_`) && familyDeclaration) {
749
+ const sub = resolveChildExportPath(
750
+ decl,
751
+ targetId.slice(id.length + 1),
752
+ );
753
+ if (sub) {
754
+ const fullExport = `${familyDeclaration.exportPath}.${sub}`;
755
+ try {
756
+ return resolveFamily(
757
+ `${familyDeclaration.module}.${fullExport}`,
758
+ { span: origin },
759
+ false,
760
+ );
761
+ } catch {}
762
+ }
763
+ }
764
+
765
+ for (const [asgnName, asgn] of assignments) {
766
+ if (targetId === asgnName || targetId.startsWith(`${asgnName}_`)) {
767
+ const tmpl = templates.get(asgn.target);
768
+ if (!tmpl) continue;
769
+ const mod = declarationSources.get(tmpl);
770
+ if (!mod || mod === "program") continue;
771
+ if (targetId === asgnName) {
772
+ try {
773
+ return resolveFamily(asgn.target, asgn, false);
774
+ } catch {}
775
+ } else {
776
+ const sub = resolveChildExportPath(
777
+ tmpl,
778
+ targetId.slice(asgnName.length + 1),
779
+ );
780
+ if (sub) {
781
+ const fullTarget = `${asgn.target}.${sub}`;
782
+ try {
783
+ return resolveFamily(fullTarget, asgn, false);
784
+ } catch {}
785
+ }
786
+ }
787
+ }
788
+ }
789
+ return undefined;
790
+ };
791
+
792
+ const checkTargetFamily = (
793
+ targetIds: string | string[],
794
+ expectedFamily: UdlFamily,
795
+ expr: { span: Span; source?: string },
796
+ ): void => {
797
+ const ids = Array.isArray(targetIds) ? targetIds : [targetIds];
798
+ for (const tid of ids) {
799
+ const fam = getInstrumentFamily(tid);
800
+ if (
801
+ !fam ||
802
+ fam.module !== expectedFamily.module ||
803
+ fam.exportPath !== expectedFamily.exportPath ||
804
+ fam.revision !== expectedFamily.revision
805
+ ) {
806
+ failWithCode(
807
+ expr,
808
+ "HSX1001",
809
+ `target instrument ${tid} family does not match expected family ${expectedFamily.module}.${expectedFamily.exportPath} (revision ${expectedFamily.revision})`,
810
+ "ensure target instrument matches the declared family",
811
+ );
812
+ }
813
+ }
814
+ };
815
+
328
816
  const enums = new Map(inheritedEnums);
329
817
  for (const parameter of decl.parameters) {
330
818
  const type =
@@ -335,18 +823,44 @@ export function compile(
335
823
  enums.set(parameter.key, type.args.map(text));
336
824
  }
337
825
  const approvers = new Set(inheritedApprovers);
338
- const supplied = entries(arguments_);
826
+ const supplied = new Map<string, Expr>();
827
+ for (const entry of arguments_.entries) {
828
+ if (supplied.has(entry.key))
829
+ fail(
830
+ entry,
831
+ `duplicate tunable ${entry.key}`,
832
+ "supply each parameter once",
833
+ );
834
+ supplied.set(entry.key, entry.value);
835
+ }
339
836
  const environment = new Map<string, Expr>(inherited);
340
837
  for (const param of decl.parameters) {
341
838
  const type =
342
839
  param.value.kind === "default" ? param.value.type : param.value;
343
840
  const fallback =
344
841
  param.value.kind === "default" ? param.value.value : undefined;
345
- const actual = supplied.get(param.key) ?? fallback;
842
+ const typeName =
843
+ type.kind === "type" || type.kind === "call" ? type.name : text(type);
844
+ const partyParameter =
845
+ attachmentInfo && (typeName === "party" || typeName === "approval");
846
+ const byName: Expr | undefined =
847
+ partyParameter &&
848
+ (subjectPartyRoles.includes(param.key as SubjectPartyRole) ||
849
+ document.parties[param.key])
850
+ ? { kind: "name", value: param.key, span: origin }
851
+ : undefined;
852
+ const actual =
853
+ supplied.get(param.key) ??
854
+ byName ??
855
+ (fallback && {
856
+ ...fallback,
857
+ source: declarationSources.get(decl) ?? "program",
858
+ });
346
859
  if (!actual) {
347
860
  if (type.kind === "type" && type.optional) continue;
348
- fail(
861
+ failWithCode(
349
862
  { span: origin },
863
+ partyParameter ? "subject_party_unbound" : "HSX1001",
350
864
  `${id} needs ${param.key}`,
351
865
  `add ${param.key}: value inside ${id}`,
352
866
  );
@@ -360,7 +874,16 @@ export function compile(
360
874
  `unknown tunable ${key}`,
361
875
  `choose ${decl.parameters.map((p) => p.key).join(", ")}`,
362
876
  );
363
- const resolve = (expr: Expr, seen = new Set<string>()): Expr => {
877
+ const isParty = (name: string) =>
878
+ !!document.parties[name] ||
879
+ (!!attachmentInfo &&
880
+ subjectPartyRoles.includes(name as SubjectPartyRole));
881
+ const resolvedParties = new Set<string>();
882
+ const resolve = (
883
+ expr: Expr,
884
+ seen = new Set<string>(),
885
+ partyBinding = false,
886
+ ): Expr => {
364
887
  if (
365
888
  expr.kind === "call" &&
366
889
  ["object", "all", "party"].includes(expr.name)
@@ -375,22 +898,33 @@ export function compile(
375
898
  const matches =
376
899
  expr.name === "party"
377
900
  ? Object.entries(document.parties)
378
- .filter(([, party]) => party.kind === type)
901
+ .filter(
902
+ ([name, party]) =>
903
+ party.kind === type && (!partyBinding || names.has(name)),
904
+ )
379
905
  .map(([name]) => name)
380
- : [...objects.values()]
906
+ : [...assignments.values()]
907
+ .filter(
908
+ (assignment) =>
909
+ expr.name === "all" ||
910
+ !attachmentSubjects.has(assignment.name) ||
911
+ attachmentSubjects.get(assignment.name) ===
912
+ attachmentInfo?.subjectKindId,
913
+ )
381
914
  .filter(
382
- (object) =>
383
- object.object === type ||
384
- type.startsWith(`${object.object}.`),
915
+ (assignment) =>
916
+ assignment.target === type ||
917
+ type.startsWith(`${assignment.target}.`),
385
918
  )
386
919
  .map(
387
- (object) =>
388
- object.name +
389
- type.slice(object.object.length).replaceAll(".", "_"),
920
+ (assignment) =>
921
+ assignment.name +
922
+ type.slice(assignment.target.length).replaceAll(".", "_"),
390
923
  );
391
924
  if (expr.name !== "all" && matches.length !== 1)
392
- fail(
393
- { span: origin },
925
+ failWithCode(
926
+ expr,
927
+ partyBinding ? "subject_party_unbound" : "HSX1001",
394
928
  `${id} needs ${expr.name === "all" ? "at least one" : "exactly one"} ${type}`,
395
929
  "declare the required object or supply this tunable explicitly",
396
930
  );
@@ -404,25 +938,46 @@ export function compile(
404
938
  : items[0]!;
405
939
  }
406
940
  if (expr.kind !== "name") return expr;
941
+ const binding = environment.get(expr.value);
942
+ if (
943
+ attachmentInfo &&
944
+ subjectPartyRoles.includes(expr.value as SubjectPartyRole) &&
945
+ (!binding ||
946
+ (binding.kind === "name" && binding.value === expr.value))
947
+ )
948
+ return expr;
949
+ if (attachmentInfo) {
950
+ const [local, ...tail] = expr.value.split(".");
951
+ const target = `${attachmentInfo.subjectKindId}_${local}`;
952
+ if (attachmentSubjects.has(target))
953
+ return { ...expr, value: [target, ...tail].join("_") };
954
+ }
407
955
  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}` };
956
+ const [, party, ...members] = expr.value.split(".");
957
+ const binding = environment.get(party!);
958
+ if (binding?.kind === "name" && isParty(binding.value))
959
+ return {
960
+ ...expr,
961
+ value: ["party", binding.value, ...members].join("."),
962
+ };
411
963
  }
412
964
  const [root, ...tail] = expr.value.split(".");
413
965
  const bound = environment.get(root!);
414
966
  if (!bound || (bound.kind === "name" && bound.value === root))
415
967
  return expr;
416
968
  if (seen.has(root!))
417
- return fail(
969
+ return failWithCode(
418
970
  expr,
971
+ partyBinding ? "subject_party_unbound" : "HSX1001",
419
972
  `cyclic tunable ${root}`,
420
973
  "replace the cycle with a literal or declared reference",
421
974
  );
422
975
  let resolved =
423
- supplied.has(root!) || inherited.has(root!) || enums.has(root!)
976
+ resolvedParties.has(root!) ||
977
+ (!partyBinding &&
978
+ (supplied.has(root!) || inherited.has(root!) || enums.has(root!)))
424
979
  ? bound
425
- : resolve(bound, new Set([...seen, root!]));
980
+ : resolve(bound, new Set([...seen, root!]), partyBinding);
426
981
  for (const key of tail) {
427
982
  if (resolved.kind !== "block") return expr;
428
983
  const child = entries(resolved).get(key);
@@ -432,7 +987,7 @@ export function compile(
432
987
  `missing tunable ${expr.value}`,
433
988
  `declare ${key} in ${root}`,
434
989
  );
435
- resolved = resolve(child, new Set([...seen, root!]));
990
+ resolved = resolve(child, new Set([...seen, root!]), partyBinding);
436
991
  }
437
992
  return resolved;
438
993
  };
@@ -443,7 +998,13 @@ export function compile(
443
998
  param.value.kind === "default" ? param.value.type : param.value;
444
999
  const type = t.kind === "type" || t.kind === "call" ? t.name : text(t);
445
1000
  const v =
446
- supplied.has(param.key) || type === "enum" ? actual : resolve(actual);
1001
+ (supplied.has(param.key) && !attachmentInfo) || type === "enum"
1002
+ ? actual
1003
+ : resolve(
1004
+ actual,
1005
+ new Set(),
1006
+ !!attachmentInfo && (type === "party" || type === "approval"),
1007
+ );
447
1008
  environment.set(param.key, v);
448
1009
  if (type === "enum" && t.kind === "call") {
449
1010
  if (v.kind !== "name" || !t.args.some((a) => text(a) === v.value))
@@ -456,12 +1017,35 @@ export function compile(
456
1017
  if (v.kind !== "list")
457
1018
  fail(v, `${param.key} needs a list`, "write [value, value]");
458
1019
  } else if (type === "party" || type === "approval") {
459
- if (v.kind !== "name" || !document.parties[v.value])
460
- fail(
461
- v,
1020
+ if (v.kind !== "name" || !isParty(v.value))
1021
+ failWithCode(
1022
+ actual,
1023
+ attachmentInfo ? "subject_party_unbound" : "HSX1001",
462
1024
  `${param.key} needs a declared party`,
463
1025
  "declare a party and use its name here",
464
1026
  );
1027
+ const party = document.parties[v.value];
1028
+ if (
1029
+ (type === "approval" && (party?.kind !== "staff" || !party.role)) ||
1030
+ (type === "party" &&
1031
+ (party?.kind === "staff" ||
1032
+ (attachmentInfo && party?.kind === "person")))
1033
+ )
1034
+ failWithCode(
1035
+ actual,
1036
+ "party_kind_mismatch",
1037
+ `${param.key} cannot bind ${v.value}`,
1038
+ type === "approval"
1039
+ ? "use a declared staff party with a role"
1040
+ : "use a subject role or declared business",
1041
+ );
1042
+ resolvedParties.add(param.key);
1043
+ if (attachmentInfo)
1044
+ attachmentInfo.parties[param.key] = subjectPartyRoles.includes(
1045
+ v.value as SubjectPartyRole,
1046
+ )
1047
+ ? { role: v.value as SubjectPartyRole }
1048
+ : { party: v.value };
465
1049
  if (type === "approval") approvers.add(text(v));
466
1050
  } else if (type === "ref") {
467
1051
  const values = v.kind === "list" ? v.items : [v];
@@ -487,14 +1071,17 @@ export function compile(
487
1071
  );
488
1072
  const [root, ...tail] = value.value.split(".");
489
1073
  const obj = objects.get(root!);
1074
+ const assignment = assignments.get(root!);
1075
+ const targetType = obj ? obj.name : assignment?.target;
490
1076
  if (
491
1077
  (!obj &&
1078
+ !assignment &&
492
1079
  !document.instruments.some(
493
1080
  (inst) => inst.id === value.value,
494
1081
  )) ||
495
1082
  (t.kind === "type" &&
496
1083
  t.target &&
497
- [obj?.object, ...tail].join(".") !== t.target)
1084
+ [targetType, ...tail].join(".") !== t.target)
498
1085
  )
499
1086
  fail(
500
1087
  value,
@@ -590,6 +1177,7 @@ export function compile(
590
1177
  for (const key of body.keys())
591
1178
  if (
592
1179
  ![
1180
+ "familyRevision",
593
1181
  "fields",
594
1182
  "lifecycle",
595
1183
  "records",
@@ -606,6 +1194,24 @@ export function compile(
606
1194
  `unknown instrument clause ${key}`,
607
1195
  "use fields, lifecycle, actions, invariants, or records",
608
1196
  );
1197
+ if (body.has("familyRevision")) {
1198
+ const revExpr = body.get("familyRevision")!;
1199
+ const revVal = literal(revExpr);
1200
+ if (
1201
+ typeof revVal !== "number" ||
1202
+ !Number.isInteger(revVal) ||
1203
+ revVal <= 0
1204
+ ) {
1205
+ fail(
1206
+ revExpr,
1207
+ "familyRevision must be a positive integer",
1208
+ "use a positive integer revision",
1209
+ );
1210
+ }
1211
+ if (familyDeclaration) {
1212
+ familyDeclaration.revision = revVal;
1213
+ }
1214
+ }
609
1215
  const records = entries(asBlock(body.get("records")));
610
1216
  if (!inherited.size)
611
1217
  environment.set("parent", { kind: "name", value: id, span: origin });
@@ -618,11 +1224,63 @@ export function compile(
618
1224
  });
619
1225
  const fields: UdlField[] = [];
620
1226
  const calculations: UdlCalculation[] = [];
1227
+ let currentAction: UdlAction | undefined;
1228
+ let currentActionName: string | undefined;
621
1229
  const path = (expr: Expr): string => {
622
1230
  const value = resolve(expr);
623
1231
  const name = text(value);
624
- if (document.parties[name]) return `party.${name}`;
625
- return /^(self|input|party)\./.test(name) ? name : `self.${name}`;
1232
+ if (isParty(name)) return `party.${name}`;
1233
+ if (name.startsWith("subject.")) {
1234
+ const subField = name.split(".")[1]!;
1235
+ if (currentAction) {
1236
+ const req = currentAction.subject?.requirements.find(
1237
+ (r) => r.field.name === subField,
1238
+ );
1239
+ if (!req) {
1240
+ failWithCode(
1241
+ expr,
1242
+ "subject_field_unknown",
1243
+ `subject.${subField} names no declared subject requirement in action ${currentActionName}`,
1244
+ `declare ${subField} in subject { ... }`,
1245
+ );
1246
+ }
1247
+ } else {
1248
+ const declaredInAction = decl.body.entries.some((e) => {
1249
+ if (!e.key.startsWith("action ")) return false;
1250
+ const subBlock = entries(asBlock(e.value)).get("subject");
1251
+ if (!subBlock) return false;
1252
+ return asBlock(subBlock).entries.some((se) => {
1253
+ if (se.key === subField) return true;
1254
+ if (se.key === "adapter") {
1255
+ const names =
1256
+ se.value.kind === "list"
1257
+ ? se.value.items.map(text)
1258
+ : [text(se.value)];
1259
+ return names.some((n) => {
1260
+ const reg = options.adapterRegistry?.[n];
1261
+ if (!reg) return false;
1262
+ const op = reg.adapter.operationMap[reg.operation];
1263
+ return op?.subjectRequirements?.some(
1264
+ (sr) => sr.name === subField,
1265
+ );
1266
+ });
1267
+ }
1268
+ return false;
1269
+ });
1270
+ });
1271
+ if (!declaredInAction) {
1272
+ failWithCode(
1273
+ expr,
1274
+ "subject_field_unknown",
1275
+ `subject.${subField} names no declared subject requirement in instrument ${id}`,
1276
+ `declare ${subField} in an action subject { ... }`,
1277
+ );
1278
+ }
1279
+ }
1280
+ }
1281
+ return /^(self|input|party|subject)\./.test(name)
1282
+ ? name
1283
+ : `self.${name}`;
626
1284
  };
627
1285
  const val = (expr: Expr): UdlValue => {
628
1286
  const v = resolve(expr);
@@ -639,8 +1297,57 @@ export function compile(
639
1297
  return id;
640
1298
  const value = resolve(expr);
641
1299
  if (value.kind === "block") {
1300
+ const rawEntries = entries(value);
1301
+ if (
1302
+ (rawEntries.has("family") && !rawEntries.has("kind")) ||
1303
+ ((rawEntries.has("states") ||
1304
+ rawEntries.has("reference") ||
1305
+ rawEntries.has("anchor")) &&
1306
+ rawEntries.has("instrument"))
1307
+ ) {
1308
+ let famTuple: UdlFamily | undefined;
1309
+ if (rawEntries.has("family")) {
1310
+ const famExpr = rawEntries.get("family")!;
1311
+ const famStr =
1312
+ famExpr.kind === "name" ? famExpr.value : text(famExpr);
1313
+ famTuple = resolveFamily(famStr, famExpr);
1314
+ }
1315
+
1316
+ let instrumentVal: unknown;
1317
+ if (rawEntries.has("instrument")) {
1318
+ instrumentVal = data(rawEntries.get("instrument")!);
1319
+ if (famTuple) {
1320
+ checkTargetFamily(
1321
+ instrumentVal as string | string[],
1322
+ famTuple,
1323
+ rawEntries.get("instrument")!,
1324
+ );
1325
+ }
1326
+ } else if (famTuple) {
1327
+ const matched = resolveFamilyInstruments(
1328
+ famTuple,
1329
+ id,
1330
+ rawEntries.get("family")!,
1331
+ );
1332
+ instrumentVal = matched.length === 1 ? matched[0] : matched;
1333
+ }
1334
+
1335
+ const result: Record<string, unknown> = {};
1336
+ if (famTuple) {
1337
+ result.family = famTuple;
1338
+ }
1339
+ if (instrumentVal !== undefined) {
1340
+ result.instrument = instrumentVal;
1341
+ }
1342
+ for (const [k, v] of rawEntries) {
1343
+ if (k === "family" || k === "instrument") continue;
1344
+ result[k] = data(v);
1345
+ }
1346
+ return result;
1347
+ }
1348
+
642
1349
  const result = Object.fromEntries(
643
- [...entries(value)].map(([key, value]) => [key, data(value)]),
1350
+ [...rawEntries].map(([key, value]) => [key, data(value)]),
644
1351
  );
645
1352
  const selection = result.selection as
646
1353
  | { instrument?: unknown }
@@ -692,66 +1399,86 @@ export function compile(
692
1399
  const t = row.value.kind === "default" ? row.value.type : row.value;
693
1400
  const constant =
694
1401
  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
- }
1402
+ const f = lowerFieldShape(row, resolve);
1403
+ const type = f.type;
724
1404
  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;
1405
+ const item = t.args[0]!;
1406
+ if (item.kind === "type" && item.name === "ref" && item.target) {
1407
+ f.target = text(
1408
+ resolve({ kind: "name", value: item.target, span: item.span }),
1409
+ ).replaceAll(".", "_");
1410
+ f.targetKind = objects.has(String(f.target))
1411
+ ? "object"
1412
+ : "instrument";
1413
+ }
731
1414
  }
732
1415
  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;
1416
+ if (t.kind === "block") {
1417
+ const b = entries(t);
1418
+ const famNode = b.get("targetFamily") ?? b.get("family");
1419
+ let targetFamTuple: UdlFamily | undefined;
1420
+ if (famNode) {
1421
+ const famStr =
1422
+ famNode.kind === "name" ? famNode.value : text(famNode);
1423
+ targetFamTuple = resolveFamily(famStr, famNode);
1424
+ f.targetFamily = targetFamTuple;
1425
+ }
1426
+ if (b.has("target") || b.has("instrument")) {
1427
+ const tgtExpr = (b.get("target") ?? b.get("instrument"))!;
1428
+ const resolvedTgt = resolve(tgtExpr);
1429
+ let tgtVal: string | string[];
1430
+ if (resolvedTgt.kind === "list") {
1431
+ const items = resolvedTgt.items.map((it) =>
1432
+ text(resolve(it)).replaceAll(".", "_"),
1433
+ );
1434
+ tgtVal = items.length === 1 ? items[0]! : items;
1435
+ } else {
1436
+ tgtVal = text(resolvedTgt).replaceAll(".", "_");
1437
+ }
1438
+ f.target = tgtVal;
1439
+ if (targetFamTuple) {
1440
+ checkTargetFamily(tgtVal, targetFamTuple, tgtExpr);
1441
+ }
1442
+ } else if (targetFamTuple) {
1443
+ const matched = resolveFamilyInstruments(
1444
+ targetFamTuple,
1445
+ id,
1446
+ famNode!,
1447
+ );
1448
+ f.target = matched.length === 1 ? matched[0] : matched;
1449
+ }
1450
+ f.targetKind =
1451
+ typeof f.target === "string" && objects.has(f.target)
1452
+ ? "object"
1453
+ : "instrument";
1454
+ } else {
1455
+ const target = t.kind === "type" ? t.target : undefined;
1456
+ if (!target)
1457
+ fail(row, "reference needs a target", "write ref<object>");
1458
+ const [root, ...tail] = target.split(".");
1459
+ const resolved = environment.get(root!);
1460
+ const resolvedTargets =
1461
+ resolved?.kind === "list"
1462
+ ? resolved.items
1463
+ : resolved
1464
+ ? [resolved]
1465
+ : [];
1466
+ const targets = resolvedTargets.map((value) =>
1467
+ [text(resolve(value)).replaceAll(".", "_"), ...tail].join("_"),
1468
+ );
1469
+ f.target =
1470
+ target === "self"
1471
+ ? id
1472
+ : targets.length === 1
1473
+ ? targets[0]
1474
+ : targets.length
1475
+ ? targets
1476
+ : target;
1477
+ f.targetKind =
1478
+ typeof f.target === "string" && objects.has(f.target)
1479
+ ? "object"
1480
+ : "instrument";
1481
+ }
755
1482
  } else if (type === "account") {
756
1483
  if (t.kind === "call") {
757
1484
  if (t.args.length < 1 || t.args.length > 4)
@@ -764,7 +1491,13 @@ export function compile(
764
1491
  f.book = t.args[1] ? text(t.args[1]) : "cash";
765
1492
  if (t.args[2]) {
766
1493
  const mode = text(t.args[2]);
767
- if (["contra", "external"].includes(mode)) f[mode] = true;
1494
+ if (mode === "external")
1495
+ fail(
1496
+ t,
1497
+ "external account mode was removed",
1498
+ "use a reservation and instruction-bound evidence",
1499
+ );
1500
+ if (mode === "contra") f.contra = true;
768
1501
  else f.key = mode;
769
1502
  }
770
1503
  if (t.args[3]) f.key = text(t.args[3]);
@@ -889,6 +1622,7 @@ export function compile(
889
1622
  lifecycle.transitions = {};
890
1623
  const inst: UdlInstrument = {
891
1624
  id,
1625
+ ...(attachmentInfo ? { subject: attachmentInfo.subjectKindId } : {}),
892
1626
  title: title(id),
893
1627
  summary: body.has("summary")
894
1628
  ? String(data(body.get("summary")!))
@@ -921,9 +1655,9 @@ export function compile(
921
1655
  // appear after the instrument that asks about its fields.
922
1656
  const bound = resolve(binding!);
923
1657
  const [root, ...children] = text(bound).split(".");
924
- const object = objects.get(root!);
925
- let target = object
926
- ? templates.get(object.object)?.body
1658
+ const assignment = assignments.get(root!);
1659
+ let target = assignment
1660
+ ? templates.get(assignment.target)?.body
927
1661
  : program.decls
928
1662
  .filter(
929
1663
  (decl): decl is InstrumentDecl =>
@@ -971,6 +1705,221 @@ export function compile(
971
1705
  }),
972
1706
  });
973
1707
  const slots = entries(selected(asBlock(row.value)));
1708
+ let actionSubject: UdlActionSubject | undefined;
1709
+ let subjectExpr = slots.get("subject");
1710
+ const boundaryBindings = new Set<string>();
1711
+ const authoredMoves = slots.get("moves");
1712
+ for (const move of authoredMoves?.kind === "list"
1713
+ ? authoredMoves.items
1714
+ : authoredMoves
1715
+ ? [authoredMoves]
1716
+ : []) {
1717
+ const parts = entries(asBlock(move));
1718
+ const boundary = parts.get("boundary");
1719
+ if (!boundary) continue;
1720
+ if (
1721
+ (parts.has("operation")
1722
+ ? String(data(parts.get("operation")!))
1723
+ : "internal_transfer.create") !== "internal_transfer.reserve" ||
1724
+ parts.has("shares") ||
1725
+ parts.has("fee")
1726
+ )
1727
+ fail(
1728
+ move,
1729
+ "boundary dispatch requires a reservation",
1730
+ "reserve the exact amount before dispatch",
1731
+ );
1732
+ const adapterExpr = entries(asBlock(boundary)).get("adapter");
1733
+ if (!adapterExpr)
1734
+ fail(
1735
+ boundary,
1736
+ "boundary needs an adapter",
1737
+ "name a bound ADL adapter",
1738
+ );
1739
+ const binding = text(resolve(adapterExpr!));
1740
+ const target =
1741
+ options.adapterRegistry &&
1742
+ Object.hasOwn(options.adapterRegistry, binding)
1743
+ ? options.adapterRegistry[binding]
1744
+ : undefined;
1745
+ if (
1746
+ !target ||
1747
+ !Object.hasOwn(target.adapter.operationMap, target.operation)
1748
+ )
1749
+ fail(
1750
+ boundary,
1751
+ `unknown boundary adapter ${binding}`,
1752
+ "bind the named ADL adapter before compilation",
1753
+ );
1754
+ boundaryBindings.add(binding);
1755
+ }
1756
+ if (boundaryBindings.size) {
1757
+ const block = asBlock(subjectExpr);
1758
+ const declared = new Set(
1759
+ block.entries
1760
+ .filter((entry) => entry.key === "adapter")
1761
+ .flatMap((entry) =>
1762
+ entry.value.kind === "list"
1763
+ ? entry.value.items.map(text)
1764
+ : [text(entry.value)],
1765
+ ),
1766
+ );
1767
+ subjectExpr = {
1768
+ ...block,
1769
+ entries: [
1770
+ ...block.entries,
1771
+ ...[...boundaryBindings]
1772
+ .filter((binding) => !declared.has(binding))
1773
+ .map((binding) => ({
1774
+ key: "adapter",
1775
+ value: {
1776
+ kind: "name" as const,
1777
+ value: binding,
1778
+ span: row.span,
1779
+ },
1780
+ span: row.span,
1781
+ })),
1782
+ ],
1783
+ };
1784
+ }
1785
+ if (subjectExpr) {
1786
+ const subjectBlock = asBlock(subjectExpr);
1787
+ const directRequirements: UdlSubjectRequirement[] = [];
1788
+ const adapterList: UdlActionSubject["adapters"] = [];
1789
+ for (const entry of subjectBlock.entries) {
1790
+ if (entry.key === "adapter") {
1791
+ const bindingNames =
1792
+ entry.value.kind === "list"
1793
+ ? entry.value.items.map(text)
1794
+ : [text(entry.value)];
1795
+ for (const bindingName of bindingNames) {
1796
+ const target = options.adapterRegistry?.[bindingName];
1797
+ if (target) {
1798
+ const { adapter, operation } = target;
1799
+ const opBinding = adapter.operationMap[operation];
1800
+ if (
1801
+ opBinding &&
1802
+ opBinding.subjectRequirements !== undefined
1803
+ ) {
1804
+ const validatedRequirements: UdlObjectField[] = [];
1805
+ for (const req of opBinding.subjectRequirements) {
1806
+ const result = udlObjectFieldSchema.safeParse(req);
1807
+ if (!result.success || result.data.optional) {
1808
+ fail(
1809
+ entry,
1810
+ `adapter requirement ${req.name} is invalid or optional: ${result.success ? "requirements cannot be optional" : result.error.message}`,
1811
+ "ensure adapter subject requirements conform to UDL schema",
1812
+ );
1813
+ }
1814
+ validatedRequirements.push(result.data);
1815
+ }
1816
+ const declaration = {
1817
+ provider: adapter.provider,
1818
+ capability: adapter.capability,
1819
+ operation,
1820
+ requirements: validatedRequirements,
1821
+ };
1822
+ const digest = sha256(
1823
+ new TextEncoder().encode(canonicalJson(declaration)),
1824
+ );
1825
+ const snapshot: UdlAdapterSubjectSnapshot = {
1826
+ ...declaration,
1827
+ declarationDigest: Array.from(digest, (byte) =>
1828
+ byte.toString(16).padStart(2, "0"),
1829
+ ).join(""),
1830
+ };
1831
+ const adapterRenames: Record<string, string> = {};
1832
+ if (attachmentInfo?.renames) {
1833
+ for (const req of snapshot.requirements) {
1834
+ if (attachmentInfo.renames.has(req.name)) {
1835
+ adapterRenames[req.name] = attachmentInfo.renames.get(
1836
+ req.name,
1837
+ )!;
1838
+ }
1839
+ }
1840
+ }
1841
+ adapterList.push({
1842
+ binding: bindingName,
1843
+ snapshot,
1844
+ ...(Object.keys(adapterRenames).length > 0
1845
+ ? { renames: adapterRenames }
1846
+ : {}),
1847
+ });
1848
+ for (const reqField of snapshot.requirements) {
1849
+ const objectField = attachmentInfo?.renames.get(
1850
+ reqField.name,
1851
+ );
1852
+ const targetName = objectField ?? reqField.name;
1853
+ const existing = directRequirements.find(
1854
+ (r) => (r.objectField ?? r.field.name) === targetName,
1855
+ );
1856
+ if (existing) {
1857
+ if (!sameObjectField(existing.field, reqField)) {
1858
+ failWithCode(
1859
+ entry,
1860
+ "subject_field_conflict",
1861
+ `conflicting requirement ${reqField.name} in action ${name}`,
1862
+ "rename or unify the requirement",
1863
+ );
1864
+ }
1865
+ } else {
1866
+ directRequirements.push({
1867
+ field: { ...reqField },
1868
+ ...(objectField ? { objectField } : {}),
1869
+ });
1870
+ }
1871
+ }
1872
+ } else {
1873
+ adapterList.push({
1874
+ binding: bindingName,
1875
+ snapshot: null,
1876
+ });
1877
+ }
1878
+ } else {
1879
+ adapterList.push({
1880
+ binding: bindingName,
1881
+ snapshot: null,
1882
+ });
1883
+ }
1884
+ }
1885
+ } else {
1886
+ const fieldDef = lowerObjectField(entry, resolve);
1887
+ if (fieldDef.optional) {
1888
+ fail(
1889
+ entry,
1890
+ `subject requirement ${entry.key} cannot be optional`,
1891
+ "remove ? from requirement",
1892
+ );
1893
+ }
1894
+ const objectField = attachmentInfo?.renames.get(entry.key);
1895
+ const targetName = objectField ?? entry.key;
1896
+ const existing = directRequirements.find(
1897
+ (r) => (r.objectField ?? r.field.name) === targetName,
1898
+ );
1899
+ if (existing) {
1900
+ if (!sameObjectField(existing.field, fieldDef)) {
1901
+ failWithCode(
1902
+ entry,
1903
+ "subject_field_conflict",
1904
+ `conflicting requirement ${entry.key} in action ${name}`,
1905
+ "rename or unify the requirement",
1906
+ );
1907
+ }
1908
+ } else {
1909
+ directRequirements.push({
1910
+ field: fieldDef,
1911
+ ...(objectField ? { objectField } : {}),
1912
+ });
1913
+ }
1914
+ }
1915
+ }
1916
+ if (directRequirements.length > 0 || adapterList.length > 0) {
1917
+ actionSubject = {
1918
+ requirements: directRequirements,
1919
+ adapters: adapterList,
1920
+ };
1921
+ }
1922
+ }
974
1923
  const a: UdlAction = {
975
1924
  summary: slots.has("summary")
976
1925
  ? String(data(slots.get("summary")!))
@@ -981,7 +1930,20 @@ export function compile(
981
1930
  input: lowerFields(asBlock(slots.get("input"))),
982
1931
  requires: [],
983
1932
  moves: [],
1933
+ ...(actionSubject ? { subject: actionSubject } : {}),
984
1934
  };
1935
+ for (const binding of boundaryBindings)
1936
+ if (
1937
+ !a.subject?.adapters.find((entry) => entry.binding === binding)
1938
+ ?.snapshot
1939
+ )
1940
+ fail(
1941
+ row,
1942
+ `boundary adapter ${binding} has no declared requirements`,
1943
+ "declare the adapter subject requirements, including an explicit empty list",
1944
+ );
1945
+ currentAction = a;
1946
+ currentActionName = name;
985
1947
  if (name !== "create") {
986
1948
  const from = slots.get("from");
987
1949
  const to = slots.get("to");
@@ -999,7 +1961,8 @@ export function compile(
999
1961
  };
1000
1962
  }
1001
1963
  for (const [key, expr] of slots) {
1002
- if (["from", "to", "input", "summary"].includes(key)) continue;
1964
+ if (["from", "to", "input", "summary", "subject"].includes(key))
1965
+ continue;
1003
1966
  if (key === "moves") {
1004
1967
  const moves = expr.kind === "list" ? expr.items : [expr];
1005
1968
  for (const [index, move] of moves.entries()) {
@@ -1033,6 +1996,25 @@ export function compile(
1033
1996
  const amount = parts.get("amount"),
1034
1997
  from = parts.get("from"),
1035
1998
  to = parts.get("to");
1999
+ if (amount) {
2000
+ const resolvedAmount = resolve(amount);
2001
+ if (
2002
+ resolvedAmount.kind === "name" &&
2003
+ resolvedAmount.value.startsWith("subject.")
2004
+ ) {
2005
+ const subField = resolvedAmount.value.split(".")[1]!;
2006
+ const req = a.subject?.requirements.find(
2007
+ (r) => r.field.name === subField,
2008
+ );
2009
+ if (req && req.field.type !== "money") {
2010
+ fail(
2011
+ amount,
2012
+ `move amount subject.${subField} must have type money`,
2013
+ "use a money field",
2014
+ );
2015
+ }
2016
+ }
2017
+ }
1036
2018
  if (parts.has("shares")) {
1037
2019
  if (
1038
2020
  !amount ||
@@ -1064,7 +2046,7 @@ export function compile(
1064
2046
  });
1065
2047
  if (
1066
2048
  recipient.kind !== "name" ||
1067
- !document.parties[recipient.value] ||
2049
+ !isParty(recipient.value) ||
1068
2050
  rate.kind !== "percent"
1069
2051
  )
1070
2052
  fail(
@@ -1139,6 +2121,19 @@ export function compile(
1139
2121
  ...transfer,
1140
2122
  operation: op,
1141
2123
  capture: String(data(parts.get("capture")!)),
2124
+ ...(parts.has("boundary")
2125
+ ? {
2126
+ boundary: {
2127
+ adapter: text(
2128
+ resolve(
2129
+ entries(
2130
+ asBlock(parts.get("boundary")),
2131
+ ).get("adapter")!,
2132
+ ),
2133
+ ),
2134
+ },
2135
+ }
2136
+ : {}),
1142
2137
  }
1143
2138
  : { ...transfer, operation: op },
1144
2139
  );
@@ -1298,7 +2293,41 @@ export function compile(
1298
2293
  a.approval = approval;
1299
2294
  } else (a as unknown as Record<string, unknown>)[key] = data(expr);
1300
2295
  }
2296
+ if (attachmentInfo) {
2297
+ if (attachmentInfo.exposed.has(name)) {
2298
+ a.publicAction = attachmentInfo.exposed.get(name)!;
2299
+ } else {
2300
+ delete a.publicAction;
2301
+ }
2302
+ }
1301
2303
  if (automatic(a.actor)) delete a.publicAction;
2304
+ const checkSubjectPaths = (obj: unknown, span: Span) => {
2305
+ if (typeof obj === "string") {
2306
+ if (obj.startsWith("subject.")) {
2307
+ const subField = obj.split(".")[1]!;
2308
+ const req = a.subject?.requirements.find(
2309
+ (r) => r.field.name === subField,
2310
+ );
2311
+ if (!req) {
2312
+ failWithCode(
2313
+ { span },
2314
+ "subject_field_unknown",
2315
+ `subject.${subField} names no declared subject requirement in action ${name}`,
2316
+ `declare ${subField} in subject { ... }`,
2317
+ );
2318
+ }
2319
+ }
2320
+ } else if (Array.isArray(obj)) {
2321
+ for (const item of obj) checkSubjectPaths(item, span);
2322
+ } else if (obj !== null && typeof obj === "object") {
2323
+ for (const val of Object.values(obj)) checkSubjectPaths(val, span);
2324
+ }
2325
+ };
2326
+ checkSubjectPaths(a.requires, row.span);
2327
+ checkSubjectPaths(a.set, row.span);
2328
+ checkSubjectPaths(a.invoke, row.span);
2329
+ currentAction = undefined;
2330
+ currentActionName = undefined;
1302
2331
  for (const requirement of a.requires ?? []) {
1303
2332
  if (
1304
2333
  requirement.kind !== "approval" ||
@@ -1308,19 +2337,39 @@ export function compile(
1308
2337
  const action = requirement.action ?? name;
1309
2338
  const key = `${id}_${action}_decision`;
1310
2339
  const previous = implicitDecisions.get(key);
1311
- if (previous && previous.party !== requirement.party)
2340
+ if (
2341
+ previous &&
2342
+ (previous.party !== requirement.party ||
2343
+ previous.protectedRequest !==
2344
+ (requirement.protectedRequest ?? "self"))
2345
+ )
1312
2346
  fail(
1313
2347
  { span: origin },
1314
- `action ${action} has multiple approval parties`,
2348
+ `action ${action} has conflicting approval parties or protected requests`,
1315
2349
  "use a separate decision action for each party",
1316
2350
  );
1317
2351
  implicitDecisions.set(key, {
1318
2352
  target: id,
1319
2353
  action,
1320
2354
  party: requirement.party,
2355
+ protectedRequest: requirement.protectedRequest ?? "self",
1321
2356
  origin,
1322
2357
  });
1323
2358
  }
2359
+ for (const requirement of a.subject?.requirements ?? []) {
2360
+ const entry =
2361
+ asBlock(subjectExpr).entries.find(
2362
+ (entry) => entry.key === requirement.field.name,
2363
+ ) ??
2364
+ asBlock(subjectExpr).entries.find(
2365
+ (entry) => entry.key === "adapter",
2366
+ );
2367
+ requirementOrigins.set(requirement, {
2368
+ source: declarationSources.get(decl) ?? "program",
2369
+ span: entry?.span ?? row.span,
2370
+ message: `${id}.${name}.subject.${requirement.field.name}`,
2371
+ });
2372
+ }
1324
2373
  inst.actions[name] = a;
1325
2374
  inst.actionOrder.push(name);
1326
2375
  }
@@ -1333,6 +2382,13 @@ export function compile(
1333
2382
  path: `$.instruments[${document.instruments.length}]`,
1334
2383
  span: { ...origin, ...lineColAt(source, origin.start) },
1335
2384
  });
2385
+ if (familyDeclaration && familyDeclaration.revision !== undefined) {
2386
+ inst.family = {
2387
+ module: familyDeclaration.module,
2388
+ exportPath: familyDeclaration.exportPath,
2389
+ revision: familyDeclaration.revision,
2390
+ };
2391
+ }
1336
2392
  document.instruments.push(inst);
1337
2393
  for (const [key, definition] of records) {
1338
2394
  const child: InstrumentDecl = {
@@ -1342,6 +2398,19 @@ export function compile(
1342
2398
  body: asBlock(definition),
1343
2399
  span: origin,
1344
2400
  };
2401
+ declarationSources.set(
2402
+ child,
2403
+ declarationSources.get(decl) ?? "program",
2404
+ );
2405
+ const childFamily = familyDeclaration
2406
+ ? {
2407
+ module: familyDeclaration.module,
2408
+ exportPath: `${familyDeclaration.exportPath}.${key}`,
2409
+ ...(familyDeclaration.revision !== undefined
2410
+ ? { revision: familyDeclaration.revision }
2411
+ : {}),
2412
+ }
2413
+ : undefined;
1345
2414
  addInstrument(
1346
2415
  child,
1347
2416
  `${id}_${key}`,
@@ -1353,9 +2422,221 @@ export function compile(
1353
2422
  ]),
1354
2423
  approvers,
1355
2424
  enums,
2425
+ attachmentInfo
2426
+ ? { ...attachmentInfo, exposed: new Map() }
2427
+ : undefined,
2428
+ childFamily,
1356
2429
  );
1357
2430
  }
1358
2431
  };
2432
+ const compileObject = (decl: ObjectDecl) => {
2433
+ const body = entries(decl.body);
2434
+ for (const key of body.keys()) {
2435
+ if (
2436
+ key !== "fields" &&
2437
+ key !== "columns" &&
2438
+ !key.startsWith("attach ")
2439
+ ) {
2440
+ fail(
2441
+ decl,
2442
+ `unknown object clause ${key}`,
2443
+ "use fields, columns, or attach",
2444
+ );
2445
+ }
2446
+ }
2447
+
2448
+ // 1. Lower authored fields
2449
+ const authoredFields: UdlObjectField[] = [];
2450
+ const authoredNames: string[] = [];
2451
+ const fieldsBlock = asBlock(body.get("fields"));
2452
+ for (const row of fieldsBlock.entries) {
2453
+ if (RESERVED_OBJECT_NAMES.some((name) => name === row.key)) {
2454
+ fail(
2455
+ row,
2456
+ `${row.key} is a reserved object name`,
2457
+ "rename this field",
2458
+ );
2459
+ }
2460
+ const fieldDef = lowerObjectField(row);
2461
+ authoredFields.push(fieldDef);
2462
+ authoredNames.push(row.key);
2463
+ }
2464
+
2465
+ // 2. Process attachments
2466
+ const attachments: UdlObjectAttachment[] = [];
2467
+ for (const entry of decl.body.entries) {
2468
+ if (!entry.key.startsWith("attach ")) continue;
2469
+ const match = /^attach\s+([A-Za-z0-9_]+)\s*=\s*(.+)$/.exec(entry.key);
2470
+ if (!match) {
2471
+ fail(
2472
+ entry,
2473
+ "invalid attach syntax",
2474
+ "write attach name = template { ... }",
2475
+ );
2476
+ }
2477
+ const attachmentName = match[1]!;
2478
+ const targetTemplate = match[2]!;
2479
+ const template = templates.get(targetTemplate);
2480
+ if (!template) {
2481
+ fail(
2482
+ entry,
2483
+ `unknown instrument ${targetTemplate}`,
2484
+ `add use ${targetTemplate.split(".")[0]} and choose a declared instrument`,
2485
+ );
2486
+ }
2487
+ const instId = `${decl.name}_${attachmentName}`;
2488
+
2489
+ const attachmentBlock = asBlock(entry.value);
2490
+ const renames = new Map<string, string>();
2491
+ const renameEntries = new Map<string, Entry>();
2492
+ const exposed = new Map<string, string>();
2493
+ const tunableEntries: Entry[] = [];
2494
+
2495
+ for (const row of attachmentBlock.entries) {
2496
+ if (row.key === "rename") {
2497
+ for (const r of asBlock(row.value).entries) {
2498
+ renames.set(r.key, text(r.value));
2499
+ renameEntries.set(r.key, r);
2500
+ }
2501
+ } else if (row.key === "expose") {
2502
+ if (row.value.kind === "call") {
2503
+ const actionName = row.value.name;
2504
+ const publicName = text(row.value.args[0]!);
2505
+ exposed.set(actionName, publicName);
2506
+ }
2507
+ } else {
2508
+ tunableEntries.push(row);
2509
+ }
2510
+ }
2511
+
2512
+ const parties: Record<string, AttachmentPartyBinding> = {};
2513
+ attachments.push({ name: attachmentName, instrument: instId, parties });
2514
+
2515
+ const tunableBlock: BlockExpr = {
2516
+ kind: "block",
2517
+ entries: tunableEntries,
2518
+ span: entry.value.span,
2519
+ };
2520
+
2521
+ const templateFamily = resolveFamily(targetTemplate, entry, false);
2522
+ addInstrument(
2523
+ template,
2524
+ instId,
2525
+ tunableBlock,
2526
+ entry.span,
2527
+ new Map(),
2528
+ new Set(),
2529
+ new Map(),
2530
+ {
2531
+ subjectKindId: decl.name,
2532
+ attachmentName,
2533
+ renames,
2534
+ exposed,
2535
+ parties,
2536
+ },
2537
+ templateFamily ? { ...templateFamily } : undefined,
2538
+ );
2539
+
2540
+ const attachedInst = document.instruments.find((i) => i.id === instId);
2541
+ if (attachedInst?.actions.create) {
2542
+ const owned = new Set(
2543
+ attachedInst.calculate.map((node) => node.target),
2544
+ );
2545
+ for (const action of Object.values(attachedInst.actions)) {
2546
+ for (const node of action.calculate ?? []) owned.add(node.target);
2547
+ for (const move of action.moves)
2548
+ if ("capture" in move && move.capture) owned.add(move.capture);
2549
+ }
2550
+ const create = attachedInst.actions.create;
2551
+ for (const field of attachedInst.fields) {
2552
+ if (
2553
+ field.type === "account" ||
2554
+ (field.type === "ref" && field.targetKind === "instrument") ||
2555
+ (field.type === "list" &&
2556
+ field.item === "ref" &&
2557
+ field.targetKind === "instrument") ||
2558
+ field.optional ||
2559
+ "value" in field ||
2560
+ owned.has(field.name)
2561
+ )
2562
+ continue;
2563
+ create.subject ??= { requirements: [], adapters: [] };
2564
+ const existing = create.subject.requirements.find(
2565
+ (item) => item.field.name === field.name,
2566
+ );
2567
+ if (existing) {
2568
+ if (canonicalJson(existing.field) !== canonicalJson(field))
2569
+ failWithCode(
2570
+ entry,
2571
+ "subject_field_conflict",
2572
+ `${instId}.create.subject.${field.name} conflicts with its instrument field`,
2573
+ "use the instrument field's type and constraints",
2574
+ );
2575
+ continue;
2576
+ }
2577
+ const objectField = renames.get(field.name);
2578
+ const requirement = {
2579
+ field,
2580
+ ...(objectField ? { objectField } : {}),
2581
+ };
2582
+ create.subject.requirements.push(requirement);
2583
+ requirementOrigins.set(requirement, {
2584
+ source: declarationSources.get(template) ?? "program",
2585
+ span: entry.span,
2586
+ message: `${instId}.create.fields.${field.name}`,
2587
+ });
2588
+ }
2589
+ }
2590
+ for (const [oldName] of renames) {
2591
+ const found =
2592
+ attachedInst &&
2593
+ Object.values(attachedInst.actions).some((action) =>
2594
+ action.subject?.requirements.some(
2595
+ (requirement) => requirement.field.name === oldName,
2596
+ ),
2597
+ );
2598
+ if (!found) {
2599
+ const renameEntry = renameEntries.get(oldName) ?? entry;
2600
+ failWithCode(
2601
+ renameEntry,
2602
+ "subject_field_unknown",
2603
+ `rename source '${oldName}' is not a declared subject requirement of ${targetTemplate}`,
2604
+ "rename a declared subject requirement",
2605
+ );
2606
+ }
2607
+ }
2608
+ }
2609
+
2610
+ // 4. Validate columns
2611
+ const columnsExpr = body.get("columns");
2612
+ let columns: string[] = [];
2613
+ if (columnsExpr) {
2614
+ if (columnsExpr.kind !== "list") {
2615
+ fail(
2616
+ columnsExpr,
2617
+ "columns needs a list of field names",
2618
+ "write columns: [name, ...]",
2619
+ );
2620
+ }
2621
+ columns = columnsExpr.items.map(text);
2622
+ if (columns.length > 8) {
2623
+ fail(
2624
+ columnsExpr,
2625
+ "at most 8 columns allowed",
2626
+ "choose up to 8 columns",
2627
+ );
2628
+ }
2629
+ }
2630
+
2631
+ document.objects.push({
2632
+ id: decl.name,
2633
+ title: decl.title,
2634
+ authoredFields: authoredNames,
2635
+ attachments,
2636
+ fields: authoredFields,
2637
+ columns,
2638
+ });
2639
+ };
1359
2640
  for (const decl of program.decls) {
1360
2641
  if (decl.kind === "instrument") {
1361
2642
  if (decl.parameters.length)
@@ -1366,15 +2647,172 @@ export function compile(
1366
2647
  );
1367
2648
  addInstrument(decl, decl.name, emptyBlock, decl.span);
1368
2649
  }
1369
- if (decl.kind === "object") {
1370
- const template = templates.get(decl.object);
2650
+ if (decl.kind === "assignment") {
2651
+ const template = templates.get(decl.target);
1371
2652
  if (!template)
1372
2653
  fail(
1373
2654
  decl,
1374
- `unknown object ${decl.object}`,
1375
- `add use ${decl.object.split(".")[0]} and choose a declared object`,
2655
+ `unknown instrument ${decl.target}`,
2656
+ `add use ${decl.target.split(".")[0]} and choose a declared instrument`,
2657
+ );
2658
+ const templateFamily = resolveFamily(decl.target, decl, false);
2659
+ addInstrument(
2660
+ template,
2661
+ decl.name,
2662
+ decl.body,
2663
+ decl.span,
2664
+ new Map(),
2665
+ new Set(),
2666
+ new Map(),
2667
+ undefined,
2668
+ templateFamily ? { ...templateFamily } : undefined,
2669
+ );
2670
+ }
2671
+ if (decl.kind === "object") {
2672
+ compileObject(decl);
2673
+ }
2674
+ }
2675
+ // Propagate mandatory invoked action requirements
2676
+ let changedInvocations = true;
2677
+ let invocationIterations = 0;
2678
+ while (changedInvocations && invocationIterations < 32) {
2679
+ changedInvocations = false;
2680
+ invocationIterations++;
2681
+ for (const inst of document.instruments) {
2682
+ for (const action of Object.values(inst.actions)) {
2683
+ for (const call of action.invoke ?? []) {
2684
+ if (call.guard) continue;
2685
+ let targetIds: string[] = [];
2686
+ if ("instrument" in call) {
2687
+ targetIds = [call.instrument];
2688
+ } else if ("selection" in call) {
2689
+ targetIds = Array.isArray(call.selection.instrument)
2690
+ ? call.selection.instrument
2691
+ : [call.selection.instrument];
2692
+ } else if ("reference" in call) {
2693
+ const refField = resolveField(
2694
+ document,
2695
+ inst,
2696
+ call.reference,
2697
+ action.input,
2698
+ action,
2699
+ );
2700
+ if (
2701
+ refField?.type === "ref" &&
2702
+ refField.targetKind === "instrument"
2703
+ ) {
2704
+ targetIds = Array.isArray(refField.target)
2705
+ ? refField.target
2706
+ : [refField.target];
2707
+ }
2708
+ }
2709
+ for (const targetId of targetIds) {
2710
+ const targetInst = document.instruments.find(
2711
+ (i) => i.id === targetId,
2712
+ );
2713
+ if (!targetInst) continue;
2714
+ if (
2715
+ inst.subject &&
2716
+ targetInst.subject &&
2717
+ inst.subject === targetInst.subject
2718
+ ) {
2719
+ const targetAction = targetInst.actions[call.action];
2720
+ if (!targetAction?.subject) continue;
2721
+ if (!action.subject) {
2722
+ action.subject = { requirements: [], adapters: [] };
2723
+ }
2724
+ for (const adapter of targetAction.subject.adapters) {
2725
+ if (
2726
+ !action.subject.adapters.some(
2727
+ (existing) => existing.binding === adapter.binding,
2728
+ )
2729
+ ) {
2730
+ action.subject.adapters.push(adapter);
2731
+ changedInvocations = true;
2732
+ }
2733
+ }
2734
+ for (const targetReq of targetAction.subject.requirements) {
2735
+ const targetObjFieldName =
2736
+ targetReq.objectField ?? targetReq.field.name;
2737
+ const existing = action.subject.requirements.find(
2738
+ (cr) =>
2739
+ (cr.objectField ?? cr.field.name) === targetObjFieldName,
2740
+ );
2741
+ if (!existing) {
2742
+ const inherited = {
2743
+ field: { ...targetReq.field, name: targetObjFieldName },
2744
+ };
2745
+ action.subject.requirements.push(inherited);
2746
+ requirementOrigins.set(
2747
+ inherited,
2748
+ requirementOrigins.get(targetReq)!,
2749
+ );
2750
+ changedInvocations = true;
2751
+ } else if (
2752
+ !sameObjectField(existing.field, targetReq.field)
2753
+ ) {
2754
+ const first = requirementOrigins.get(existing)!;
2755
+ const second = requirementOrigins.get(targetReq)!;
2756
+ throw new CompileFailure({
2757
+ code: "subject_field_conflict",
2758
+ message: `${first.message} conflicts with ${second.message}: incompatible requirement '${targetObjFieldName}'`,
2759
+ fix: "ensure compatible requirement definitions across invoked actions",
2760
+ span: first.span,
2761
+ source: first.source,
2762
+ related: [first, second],
2763
+ });
2764
+ }
2765
+ }
2766
+ }
2767
+ }
2768
+ }
2769
+ }
2770
+ }
2771
+ }
2772
+ for (const kind of document.objects) {
2773
+ const origins = new Map(
2774
+ kind.fields.map((field) => [
2775
+ field.name,
2776
+ `authored field ${kind.id}.${field.name}`,
2777
+ ]),
2778
+ );
2779
+ for (const instrument of document.instruments.filter(
2780
+ (item) => item.subject === kind.id,
2781
+ )) {
2782
+ for (const name of instrument.actionOrder) {
2783
+ for (const requirement of instrument.actions[name]!.subject
2784
+ ?.requirements ?? []) {
2785
+ const field = {
2786
+ ...requirement.field,
2787
+ name: requirement.objectField ?? requirement.field.name,
2788
+ };
2789
+ const previous = kind.fields.find(
2790
+ (item) => item.name === field.name,
2791
+ );
2792
+ const origin = `${instrument.id}.${name}.subject.${requirement.field.name}`;
2793
+ if (previous && !sameObjectField(previous, field)) {
2794
+ failWithCode(
2795
+ objects.get(kind.id)!,
2796
+ "subject_field_conflict",
2797
+ `${origin} conflicts with ${origins.get(field.name)}: incompatible types or constraints`,
2798
+ "rename fields with different meanings",
2799
+ );
2800
+ }
2801
+ if (!previous) {
2802
+ kind.fields.push(field);
2803
+ origins.set(field.name, origin);
2804
+ }
2805
+ }
2806
+ }
2807
+ }
2808
+ for (const column of kind.columns) {
2809
+ if (!kind.fields.some((field) => field.name === column))
2810
+ failWithCode(
2811
+ objects.get(kind.id)!,
2812
+ "subject_field_unknown",
2813
+ `column ${column} is unknown on ${kind.id}`,
2814
+ "name an authored field or attached requirement",
1376
2815
  );
1377
- addInstrument(template, decl.name, decl.body, decl.span);
1378
2816
  }
1379
2817
  }
1380
2818
  const eliminatedStates = new Map<string, Set<string>>();
@@ -1472,12 +2910,33 @@ export function compile(
1472
2910
  (action) =>
1473
2911
  action.approval?.action === decision.action &&
1474
2912
  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
- ),
2913
+ inst.fields.some((field) => {
2914
+ if (
2915
+ field.type !== "ref" ||
2916
+ field.target !== decision.target ||
2917
+ `self.${field.name}` !== action.approval?.target
2918
+ )
2919
+ return false;
2920
+ const [root, name, ...tail] =
2921
+ decision.protectedRequest.split(".");
2922
+ const input = name && action.approval.input[name];
2923
+ const request =
2924
+ root === "self"
2925
+ ? [action.approval.target, name, ...tail]
2926
+ .filter(Boolean)
2927
+ .join(".")
2928
+ : root === "input" && name
2929
+ ? input && "field" in input
2930
+ ? [input.field, ...tail].join(".")
2931
+ : materialApprovals.has(action)
2932
+ ? [`self.material_${name}`, ...tail].join(".")
2933
+ : undefined
2934
+ : undefined;
2935
+ return (
2936
+ request !== undefined &&
2937
+ action.approval.protectedRequest === request
2938
+ );
2939
+ }),
1481
2940
  ),
1482
2941
  );
1483
2942
  if (existing) continue;
@@ -1487,6 +2946,16 @@ export function compile(
1487
2946
  `implicit decision name ${id} is already used`,
1488
2947
  "rename the conflicting object",
1489
2948
  );
2949
+ const protectedRequest =
2950
+ decision.protectedRequest === "self"
2951
+ ? "self.target"
2952
+ : decision.protectedRequest.startsWith("self.")
2953
+ ? `self.target.${decision.protectedRequest.slice(5)}`
2954
+ : fail(
2955
+ { span: decision.origin },
2956
+ "implicit approval needs a stored protected request",
2957
+ "declare an explicit decision for an input-based protected request",
2958
+ );
1490
2959
  addInstrument(
1491
2960
  template,
1492
2961
  id,
@@ -1497,10 +2966,14 @@ export function compile(
1497
2966
  for: decision.target,
1498
2967
  approved_by: decision.party,
1499
2968
  action: decision.action,
2969
+ protected_request: protectedRequest,
1500
2970
  }).map(([key, value]) => ({
1501
2971
  key,
1502
2972
  value: {
1503
- kind: key === "action" ? "text" : "name",
2973
+ kind:
2974
+ key === "action" || key === "protected_request"
2975
+ ? "text"
2976
+ : "name",
1504
2977
  value,
1505
2978
  span: decision.origin,
1506
2979
  },
@@ -1508,6 +2981,22 @@ export function compile(
1508
2981
  })),
1509
2982
  },
1510
2983
  decision.origin,
2984
+ new Map(),
2985
+ new Set(),
2986
+ new Map(),
2987
+ document.instruments.find(
2988
+ (instrument) => instrument.id === decision.target,
2989
+ )?.subject
2990
+ ? {
2991
+ subjectKindId: document.instruments.find(
2992
+ (instrument) => instrument.id === decision.target,
2993
+ )!.subject!,
2994
+ attachmentName: id,
2995
+ parties: {},
2996
+ renames: new Map(),
2997
+ exposed: new Map(),
2998
+ }
2999
+ : undefined,
1511
3000
  );
1512
3001
  }
1513
3002
  }
@@ -1578,7 +3067,7 @@ export function compile(
1578
3067
  .find((o) => i.path.startsWith(o.path));
1579
3068
  return diagnostic(
1580
3069
  {
1581
- code: "HSX1601",
3070
+ code: i.code.startsWith("UDL") ? "HSX1601" : i.code,
1582
3071
  message: `${i.path}: ${i.message}`,
1583
3072
  fix: i.fix,
1584
3073
  span: origin?.span ?? program.span,