@githolon/dsl 0.114.0 → 0.115.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 (67) hide show
  1. package/assets/operation-contract.wasm +0 -0
  2. package/dart/CHANGELOG.md +40 -0
  3. package/dart/lib/src/application_framework_calls.dart +7 -1
  4. package/dart/lib/src/console_enrolment.dart +7 -0
  5. package/dart/lib/src/console_http_io.dart +22 -15
  6. package/dart/lib/src/dev_cloud.dart +62 -0
  7. package/dart/lib/src/operation_receipts.dart +74 -7
  8. package/dart/lib/src/reception/dart_reception.dart +26 -6
  9. package/dart/lib/src/runner/node-entry.mjs +7129 -4424
  10. package/dart/lib/src/runtime_v2_transport.dart +3 -0
  11. package/dart/pubspec.lock +1 -1
  12. package/dart/pubspec.yaml +2 -2
  13. package/dart/test/console_enrolment_test.dart +47 -0
  14. package/dart/test/model_adoption_test.dart +49 -0
  15. package/dart/test/runtime_projections_test.dart +41 -0
  16. package/dart_types/CHANGELOG.md +24 -0
  17. package/dart_types/lib/src/approved_model.dart +78 -1
  18. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +528 -2
  19. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +24 -0
  20. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +1897 -112
  21. package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pb.dart +98 -0
  22. package/dart_types/lib/src/proto/nomos/runtime/v2/records_desk.pb.dart +16 -0
  23. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +43 -2
  24. package/dart_types/pubspec.yaml +1 -1
  25. package/package.json +4 -2
  26. package/src/aggregate.ts +93 -0
  27. package/src/anonymous_value_interning.ts +205 -0
  28. package/src/baseline.ts +27 -0
  29. package/src/baseline_export.ts +432 -0
  30. package/src/baseline_import.ts +312 -0
  31. package/src/baseline_usda.ts +186 -0
  32. package/src/birth_key_rule.ts +472 -0
  33. package/src/births.ts +207 -24
  34. package/src/build_provenance.ts +10 -0
  35. package/src/business_birth.ts +50 -4
  36. package/src/check_package_main.ts +50 -6
  37. package/src/codegen_call_proto.ts +5 -18
  38. package/src/codegen_dart.ts +95 -3
  39. package/src/codegen_dart_client.ts +52 -6
  40. package/src/codegen_dart_facade.ts +57 -35
  41. package/src/codegen_dart_framework_calls.ts +20 -1
  42. package/src/codegen_proof.ts +5 -2
  43. package/src/codegen_result_proto.ts +8 -6
  44. package/src/codegen_result_usda.ts +4 -0
  45. package/src/codegen_ts.ts +109 -10
  46. package/src/compatibility_graph.ts +41 -4
  47. package/src/compatibility_usda.ts +11 -1
  48. package/src/compile_package_main.ts +214 -51
  49. package/src/composed_wire_identity.ts +24 -0
  50. package/src/directive.ts +6 -0
  51. package/src/domain_requirements.ts +75 -0
  52. package/src/engine_javascript.ts +12 -6
  53. package/src/fields.ts +109 -0
  54. package/src/identity_usda.ts +77 -51
  55. package/src/index.ts +25 -1
  56. package/src/intent.ts +5 -0
  57. package/src/law_graph.ts +142 -1
  58. package/src/law_graph_usda.ts +25 -141
  59. package/src/law_layer_prims.ts +199 -0
  60. package/src/law_unit_selection.ts +45 -0
  61. package/src/law_version.ts +107 -0
  62. package/src/package_config.ts +44 -0
  63. package/src/package_dependencies.ts +8 -0
  64. package/src/value_object.ts +6 -3
  65. package/src/wire.ts +2 -0
  66. package/src/wire_paths.ts +17 -0
  67. package/src/workspace_routing.ts +1 -1
package/src/codegen_ts.ts CHANGED
@@ -35,6 +35,7 @@ import { refusalContracts, tsRefusalDecoder, tsRefusalDecoders } from "./codegen
35
35
  * Like every emitter here: NO FALLBACK. An unsupported zod shape throws at compile
36
36
  * time, never degrades to `any` silently.
37
37
  */
38
+ import { assertDomainRequirements, type DomainUnitRequirement } from "./domain_requirements.js";
38
39
  import type { z } from "zod";
39
40
  import { generateTsWorkspaceAccessRead } from "./codegen_workspace_access.js";
40
41
 
@@ -45,6 +46,7 @@ import {
45
46
  OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
46
47
  type AggregateHandle,
47
48
  } from "./aggregate.js";
49
+ import { BUSINESS_BIRTH_KEY_PAYLOAD_FIELD } from "./births.js";
48
50
  import {
49
51
  isGitObjectIdSchema,
50
52
  isSha256DigestSchema,
@@ -86,6 +88,7 @@ import {
86
88
  collectValueObjectDescriptors,
87
89
  idOfMetaOf,
88
90
  idOfTargetKindOf,
91
+ brandedIdMetaOf,
89
92
  refOfMetaOf,
90
93
  versionedRefOfMetaOf,
91
94
  voMetaOf,
@@ -230,6 +233,13 @@ export const tsApplicationName = (packageName: string): string =>
230
233
  export const tsHashConstName = (packageName: string): string =>
231
234
  `${upperSnake(packageName)}_DOMAIN_HASH`;
232
235
 
236
+ /** `tool-library` → `TOOL_LIBRARY_DOMAIN_REQUIREMENTS` — the per-domain unit requirements const (CAP-666).
237
+ * Module-private: the sealed Reception exports only the receiver, the definition, the value helpers and
238
+ * the `<domain>Client` factories, so the table reaches tenants as `<Name>ApplicationDefinition
239
+ * .compatibility.domainRequirements`, mirroring Dart's `frameworkCalls.domainRequirements`. */
240
+ export const tsDomainRequirementsConstName = (packageName: string): string =>
241
+ `${upperSnake(packageName)}_DOMAIN_REQUIREMENTS`;
242
+
233
243
  /** `tool-library` → `TOOL_LIBRARY_INTERFACE` — the baked structural-interface const (#72). */
234
244
  export const interfaceConstName = (packageName: string): string =>
235
245
  `${upperSnake(packageName)}_COMPATIBILITY_MATRIX_USDA`;
@@ -431,6 +441,18 @@ function mintLazyTypeName(reg: LazyTypeRegistry, path: string): string {
431
441
  * (never `JsonValue`) — see {@link LazyTypeRegistry}. `reg` is absent only for standalone callers
432
442
  * (e.g. a query return) that can't host a named decl; a recursive lazy there is the one place the
433
443
  * `JsonValue` safety valve applies (query returns are read models, never recursive in practice). */
444
+ /** BRANDED-ID ALIASES REACHED WHILE GENERATING ONE CLIENT. {@link generateTsClient} opens the sink and
445
+ * emits one `export type <Brand> = string & { readonly __brand }` per name it collected, so every
446
+ * `brandedId(...)` / `t.brandedId(...)` / `t.uuid()` value is typed by its brand in payloads AND read models.
447
+ * A standalone caller with no open sink keeps the plain `string` (it has nowhere to declare an alias). */
448
+ let brandedIdSink: Set<string> | undefined;
449
+
450
+ function brandedIdTsType(brand: string): string {
451
+ if (brandedIdSink === undefined) return "string";
452
+ brandedIdSink.add(brand);
453
+ return brand;
454
+ }
455
+
434
456
  function tsTypeOfZod(
435
457
  zt: z.ZodTypeAny,
436
458
  indent: string,
@@ -468,6 +490,8 @@ function tsTypeOfZod(
468
490
  : aggregateType;
469
491
  return `NomosRef<${target}>`;
470
492
  }
493
+ const brand = brandedIdMetaOf(zt);
494
+ if (brand !== undefined && zodKind(zt) === "string") return brandedIdTsType(brand);
471
495
  // A DECLARED VALUE OBJECT keeps its NAME (vo_first_class.md): the payload
472
496
  // references the same exported interface the read model uses — one name, both directions —
473
497
  // instead of an anonymous inline literal type. Checked on the BARE object only: optional/
@@ -882,12 +906,15 @@ export function tsTypeOfField(field: Field): string {
882
906
 
883
907
  function tsBaseTypeOfField(field: Field): string {
884
908
  switch (field.kind) {
885
- case "string":
886
- return isSha256DigestSchema(field.zod as z.ZodTypeAny)
887
- ? "Sha256Digest"
888
- : isGitObjectIdSchema(field.zod as z.ZodTypeAny)
889
- ? "GitObjectId"
890
- : "string";
909
+ case "string": {
910
+ const schema = field.zod as z.ZodTypeAny;
911
+ if (isSha256DigestSchema(schema)) return "Sha256Digest";
912
+ if (isGitObjectIdSchema(schema)) return "GitObjectId";
913
+ // TYPED IDS on aggregate fields (`t.brandedId(Name)` / `t.uuid()`): the read model types them exactly as
914
+ // a payload of the same schema — never a bare string.
915
+ const brand = brandedIdMetaOf(schema);
916
+ return brand !== undefined ? brandedIdTsType(brand) : "string";
917
+ }
891
918
  case "int":
892
919
  return "number";
893
920
  // STRUCTURAL CRDT scalars (read-collapsed): a counter reads as its net number, a position as its
@@ -1131,6 +1158,10 @@ export interface TsClientOptions {
1131
1158
  /** THE LAW this client is generated against: the unit set identity of `build/<name>.units` (CAP-773,
1132
1159
  * law_unit_set_identity.ts). Never the package closure, and never sent to the kernel with an offer. */
1133
1160
  readonly domainHash: string;
1161
+ /** THE PER-DOMAIN REQUIREMENTS (CAP-666): per application domain key, its unit hash and its generated
1162
+ * requirement identity. Carried by the definition's `compatibility.domainRequirements` only when present
1163
+ * (the era rule). */
1164
+ readonly domainRequirements?: readonly DomainUnitRequirement[];
1134
1165
  /** Sibling lossless USDZ transport carried by the generated client when its signed-in workspace
1135
1166
  * still serves the predecessor model. The archive is never canonical state and is not embedded. */
1136
1167
  readonly lawAssetName?: string;
@@ -1421,6 +1452,20 @@ function zodUsesGitObjectId(
1421
1452
  export function generateTsClient(
1422
1453
  modules: readonly DomainModule[],
1423
1454
  opts: TsClientOptions
1455
+ ): string {
1456
+ const outerSink = brandedIdSink;
1457
+ brandedIdSink = new Set<string>();
1458
+ try {
1459
+ return generateTsClientBody(modules, opts, brandedIdSink);
1460
+ } finally {
1461
+ brandedIdSink = outerSink;
1462
+ }
1463
+ }
1464
+
1465
+ function generateTsClientBody(
1466
+ modules: readonly DomainModule[],
1467
+ opts: TsClientOptions,
1468
+ brandSink: Set<string>,
1424
1469
  ): string {
1425
1470
  if (opts.callUsda === undefined) {
1426
1471
  throw new Error(
@@ -1451,6 +1496,8 @@ export function generateTsClient(
1451
1496
  )
1452
1497
  );
1453
1498
  const topologyModules = modules;
1499
+ // Branded-id aliases already declared (by the value-object block); the rest are declared at the end.
1500
+ const emittedBrands = new Set<string>();
1454
1501
  // A public business birth can target a deliberately hidden root aggregate. Its private binder still
1455
1502
  // needs a nominal type, but unrelated framework records must remain absent even from generated source.
1456
1503
  const publicBusinessAggregateIds = new Set(
@@ -1589,6 +1636,8 @@ export function generateTsClient(
1589
1636
  opts.interface as unknown as Record<string, DomainInterface>
1590
1637
  );
1591
1638
  const hashConst = tsHashConstName(opts.packageName);
1639
+ const domainRequirements = [...(opts.domainRequirements ?? [])].sort((a, b) => a.domainKey < b.domainKey ? -1 : a.domainKey > b.domainKey ? 1 : 0);
1640
+ assertDomainRequirements(domainRequirements);
1592
1641
  // THE E2E-SEALING MAP (the sealing client): non-null iff any field is `.encrypted()`. When null,
1593
1642
  // NOTHING below emits — the generated client is byte-identical to a pre-sealing compile.
1594
1643
  const encMap = buildEncryptionMap(
@@ -2216,6 +2265,20 @@ export function generateTsClient(
2216
2265
  ` * workspace serves a closure its own kernel composes from those units; compatibility is the kernel's verdict. */`,
2217
2266
  `const ${hashConst} = ${JSON.stringify(opts.domainHash)};`,
2218
2267
  ``,
2268
+ ...(domainRequirements.length === 0
2269
+ ? []
2270
+ : [
2271
+ `/** THE PER-DOMAIN REQUIREMENTS (CAP-666) this client was generated against: for each application domain,`,
2272
+ ` * the hash of its law unit (\`domain:<key>\` of the unit set) and the sha256 of its generated compatibility`,
2273
+ ` * requirement. A workspace serving exactly that unit for the key is current for this build whatever else`,
2274
+ ` * its unit set carries. Identities the adoption inventory and the support policy name; never a verdict. */`,
2275
+ `const ${tsDomainRequirementsConstName(opts.packageName)} = Object.freeze({`,
2276
+ ...domainRequirements.map((requirement) =>
2277
+ ` ${JSON.stringify(requirement.domainKey)}: Object.freeze({ domainKey: ${JSON.stringify(requirement.domainKey)}, `
2278
+ + `unitSha256: ${JSON.stringify(requirement.unitSha256)}, requirementSha256: ${JSON.stringify(requirement.requirementSha256)} }),`),
2279
+ `} as const);`,
2280
+ ``,
2281
+ ]),
2219
2282
  ...(clientInterface !== undefined
2220
2283
  ? [
2221
2284
  `/** Canonical compiler-authored USDA compatibility matrix. The application carries`,
@@ -3729,6 +3792,7 @@ export function generateTsClient(
3729
3792
  for (const [, d] of voByName)
3730
3793
  for (const b of voBrandNames(d)) brands.add(b);
3731
3794
  for (const b of [...brands].sort()) {
3795
+ emittedBrands.add(b);
3732
3796
  out.push(
3733
3797
  `/** Branded id \`${b}\` — a nominal string (\`brandedId("${b}")\`); wire-identical to a string. */`
3734
3798
  );
@@ -4697,7 +4761,7 @@ export function generateTsClient(
4697
4761
  publicPayloadSchema: z.ZodTypeAny;
4698
4762
  rootId: string;
4699
4763
  namespace: string;
4700
- keyField: string;
4764
+ keyField?: string;
4701
4765
  };
4702
4766
  }
4703
4767
  ).businessBirth;
@@ -4926,17 +4990,29 @@ export function generateTsClient(
4926
4990
  businessBirth === undefined || encFields === undefined
4927
4991
  ? undefined
4928
4992
  : Object.values(encFields).find((field) => field.owned === true);
4993
+ // A minted birth (the default) has no business key, but an owned birth seals to the child's address
4994
+ // before dispatch: reserve the key from the kernel's own minter and hand it to the law (CAP-804).
4995
+ const mintedOwnedBirth =
4996
+ ownedBirthDescriptor !== undefined && businessBirth!.keyField === undefined;
4929
4997
  const ownedBirthPrelude =
4930
4998
  ownedBirthDescriptor === undefined
4931
4999
  ? ""
4932
5000
  : ` if (!holon.__nomosOwnedBirthScopeKey) throw new Error("this holon predates automatic .owned() birth custody — upgrade @githolon/runtime");\n` +
5001
+ (mintedOwnedBirth
5002
+ ? ` if (!holon.mint) throw new Error("this holon predates minted birth keys — upgrade @githolon/runtime");\n` +
5003
+ ` const _birthKey = await holon.mint(${JSON.stringify(
5004
+ businessBirth!.aggregateId
5005
+ )});\n`
5006
+ : "") +
4933
5007
  ` const _ownedBirthKey = await holon.__nomosOwnedBirthScopeKey({ scope: ${JSON.stringify(
4934
5008
  ownedBirthDescriptor.scope
4935
5009
  )}, namespace: ${JSON.stringify(
4936
5010
  businessBirth!.namespace
4937
- )}, key: ${inputPayload}[${JSON.stringify(
4938
- businessBirth!.keyField
4939
- )}], record: { type: ${JSON.stringify(
5011
+ )}, key: ${
5012
+ mintedOwnedBirth
5013
+ ? "_birthKey"
5014
+ : `${inputPayload}[${JSON.stringify(businessBirth!.keyField)}]`
5015
+ }, record: { type: ${JSON.stringify(
4940
5016
  ownedRuntimeType
4941
5017
  )}, id: ${JSON.stringify(businessBirth!.rootId)} } });`;
4942
5018
  const paramSig = `${argumentName}: ${payloadName}`;
@@ -5268,6 +5344,10 @@ export function generateTsClient(
5268
5344
  type: `${payloadName} & { readonly principal: string; readonly bornAt: string${ownedSeedFields} }`,
5269
5345
  });
5270
5346
  }
5347
+ // A minted birth's law admits a key the client reserved ahead of dispatch (CAP-804).
5348
+ if (businessBirth !== undefined && businessBirth.keyField === undefined) {
5349
+ callExtraFields.set(BUSINESS_BIRTH_KEY_PAYLOAD_FIELD, { type: "string", optional: true });
5350
+ }
5271
5351
  // The public payload remains purely application-authored. The private encoder
5272
5352
  // type additionally admits the custody facts injected by the generated client
5273
5353
  // after it obtains them from the framework-owned key ceremony.
@@ -5420,6 +5500,10 @@ export function generateTsClient(
5420
5500
  ownedBirthDescriptor !== undefined
5421
5501
  ? `, [_OWNED_KEY_PAYLOAD_FIELD]: _ownedBirthKey.ownedKeySubject`
5422
5502
  : ""
5503
+ }${
5504
+ ownedBirthDescriptor !== undefined && businessBirth.keyField === undefined
5505
+ ? `, ${JSON.stringify(BUSINESS_BIRTH_KEY_PAYLOAD_FIELD)}: _birthKey`
5506
+ : ""
5423
5507
  } }`
5424
5508
  )};`,
5425
5509
  ` const result = await holon.offerCall(_call, { actor: principal, returnCreated: true });`,
@@ -7076,6 +7160,7 @@ export function generateTsClient(
7076
7160
  ` compatibility: {`,
7077
7161
  ` lawIdentity: sha256Digest(${hashConst}),`,
7078
7162
  ` matrixUsda: ${clientInterface === undefined ? "new Uint8Array()" : interfaceConstName(opts.packageName)},`,
7163
+ ...(domainRequirements.length === 0 ? [] : [` domainRequirements: ${tsDomainRequirementsConstName(opts.packageName)},`]),
7079
7164
  ` readyCommands: [${rootBirths
7080
7165
  .map(
7081
7166
  (birth) =>
@@ -7093,6 +7178,20 @@ export function generateTsClient(
7093
7178
  );
7094
7179
  }
7095
7180
 
7181
+ // TYPED-ID ALIASES reached by payloads and read models (beyond those the value objects declared above).
7182
+ // TS hoists type aliases, so declaring them last is fine; sorted for byte-determinism.
7183
+ const remainingBrands = [...brandSink].filter((brand) => !emittedBrands.has(brand)).sort();
7184
+ if (remainingBrands.length > 0) {
7185
+ out.push(`// ── typed ids (brandedId / t.brandedId / t.uuid) — nominal strings, wire-identical to a string ──`);
7186
+ for (const b of remainingBrands) {
7187
+ out.push(
7188
+ `/** Branded id \`${b}\` — a nominal string (\`brandedId("${b}")\`); wire-identical to a string. */`,
7189
+ `export type ${b} = string & { readonly __brand: "${b}" };`,
7190
+ ``
7191
+ );
7192
+ }
7193
+ }
7194
+
7096
7195
  // RECURSIVE PAYLOAD TYPES (`z.lazy`): emit each minted named alias once. Sorted by name for
7097
7196
  // byte-determinism; TS hoists type aliases so referencing them above is fine.
7098
7197
  if (lazyReg.decls.size > 0) {
@@ -18,6 +18,7 @@ import type { Field } from "./fields.js";
18
18
  import type { GeometryKind } from "./value_object.js";
19
19
  import { isBusinessIntent } from "./intent.js";
20
20
  import { isFrameworkAggregate } from "./framework/framework_aggregates.js";
21
+ import { composedWireIdentity } from "./composed_wire_identity.js";
21
22
  import type {
22
23
  LawAnonymousValueTypeNode,
23
24
  LawAggregateNode,
@@ -42,6 +43,21 @@ export interface PayloadFieldShape {
42
43
  readonly type?: string;
43
44
  }
44
45
 
46
+ /**
47
+ * One member of a nested record value. Two laws can agree on every nested name and kind while
48
+ * their generated codecs disagree on the Protobuf field numbers (the 1 October 2026
49
+ * `EstateAsset.canvasRequirements{}.isRequired` incident), so the member carries the allocation
50
+ * the generated codec actually encodes and decodes it under, plus any retained aliases.
51
+ */
52
+ export interface CompatibilityRecordField {
53
+ readonly required: boolean;
54
+ readonly type: string;
55
+ /** Canonical Protobuf allocation the generated codec uses for this member. */
56
+ readonly wireKey?: string;
57
+ /** Previously shipped allocations this codec still decodes during an overlap window. */
58
+ readonly wireAliases?: readonly string[];
59
+ }
60
+
45
61
  /**
46
62
  * The compatibility vocabulary for domain values. This is a graph rather than
47
63
  * an inline tree so recursive values and shared nested value objects remain
@@ -68,9 +84,7 @@ export type CompatibilityValueShape =
68
84
  | { readonly kind: "map"; readonly value: string }
69
85
  | {
70
86
  readonly kind: "record";
71
- readonly fields: Readonly<
72
- Record<string, { readonly required: boolean; readonly type: string }>
73
- >;
87
+ readonly fields: Readonly<Record<string, CompatibilityRecordField>>;
74
88
  }
75
89
  | { readonly kind: "sum"; readonly variants: readonly string[] }
76
90
  | { readonly kind: "recursive"; readonly body: string };
@@ -352,10 +366,31 @@ export function domainInterface(
352
366
  * workspace offers are now two projections of the same graph/USDA, rather than
353
367
  * independently interpreting DomainModule and hoping the shapes coincide.
354
368
  */
355
- export function domainInterfaceFromLawGraph(graph: LawGraph): DomainInterface {
369
+ export function domainInterfaceFromLawGraph(
370
+ graph: LawGraph,
371
+ /** The composed law layer's path index. Nested members of a record that OpenUSD composition
372
+ * shares with another contract take the canonical prim's allocation, exactly as the generated
373
+ * codecs do through `composedWireIdentity`; without the index each member's own identity is
374
+ * its allocation. */
375
+ paths: ReadonlyMap<string, string> = new Map()
376
+ ): DomainInterface {
356
377
  const nodes = new Map<string, LawGraph["nodes"][number]>(
357
378
  graph.nodes.map((node) => [node.entity.id, node])
358
379
  );
380
+ // The nested allocation is the one fact the generated Dart/TS codecs and installed law must
381
+ // agree on. It is derived here from the same identity the codecs derive it from, never from
382
+ // the member's name.
383
+ const nestedWire = (entity: {
384
+ readonly id: string;
385
+ readonly sid: string;
386
+ readonly wireKey?: string;
387
+ readonly wireAliases?: readonly string[];
388
+ }): Pick<CompatibilityRecordField, "wireKey" | "wireAliases"> => ({
389
+ wireKey: composedWireIdentity(paths, entity),
390
+ ...(entity.wireAliases === undefined || entity.wireAliases.length === 0
391
+ ? {}
392
+ : { wireAliases: entity.wireAliases }),
393
+ });
359
394
  const sorted = <T>(
360
395
  entries: readonly (readonly [string, T])[]
361
396
  ): Record<string, T> =>
@@ -587,6 +622,7 @@ export function domainInterfaceFromLawGraph(graph: LawGraph): DomainInterface {
587
622
  {
588
623
  required: field.required,
589
624
  type: resolvedTypeId(field.value.id),
625
+ ...nestedWire(field.entity),
590
626
  },
591
627
  ] as const,
592
628
  ];
@@ -653,6 +689,7 @@ export function domainInterfaceFromLawGraph(graph: LawGraph): DomainInterface {
653
689
  {
654
690
  required: field.required,
655
691
  type: resolvedTypeId(field.value.id),
692
+ ...nestedWire(field.entity),
656
693
  },
657
694
  ] as const,
658
695
  ];
@@ -60,10 +60,14 @@ function canonicalCompatibilityTypes(
60
60
  value = `sum(${type.variants.map(ref).sort().map(fingerprintAtom).join("")})`;
61
61
  break;
62
62
  case "record":
63
+ // Two records with identical names and kinds but different nested allocations are
64
+ // different contracts; interning them as one would erase the very evidence the kernel
65
+ // judges, so the allocation and its aliases are part of the fingerprint.
63
66
  value = `record(${Object.entries(type.fields)
64
67
  .sort(([left], [right]) => left.localeCompare(right))
65
68
  .map(([name, field]) =>
66
- `${fingerprintAtom(name)}${field.required ? "1" : "0"}${fingerprintAtom(ref(field.type))}`)
69
+ `${fingerprintAtom(name)}${field.required ? "1" : "0"}${fingerprintAtom(ref(field.type))}` +
70
+ `${fingerprintAtom(field.wireKey ?? "")}${[...(field.wireAliases ?? [])].sort().map(fingerprintAtom).join("")}`)
67
71
  .join("")})`;
68
72
  break;
69
73
  }
@@ -146,6 +150,12 @@ function emitType(lines: string[], type: CompatibilityValueShape, typeRef: (id:
146
150
  ` def NomosCompatibilityValueField "${numbered("f", index)}"`,
147
151
  " {",
148
152
  ` custom uniform string nomos:name = ${usdaString(name)}`,
153
+ ...(field.wireKey === undefined ? [] : [
154
+ ` custom uniform string nomos:wireKey = ${usdaString(field.wireKey)}`,
155
+ ]),
156
+ ...(field.wireAliases === undefined || field.wireAliases.length === 0 ? [] : [
157
+ ` custom uniform string[] nomos:wireAliases = [${field.wireAliases.map(usdaString).join(", ")}]`,
158
+ ]),
149
159
  ` custom uniform bool nomos:required = ${field.required}`,
150
160
  ` custom rel nomos:type = <${typeRef(field.type)}>`,
151
161
  " }",