@githolon/dsl 0.101.6 → 0.102.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 (120) hide show
  1. package/dart/CHANGELOG.md +13 -0
  2. package/dart/lib/nomos_client.dart +3 -52
  3. package/dart/lib/src/dev_cloud.dart +5 -0
  4. package/dart/lib/src/home.dart +10 -15
  5. package/dart/lib/src/nms_conformance_runtime.dart +12 -0
  6. package/dart/lib/src/nms_generated_runtime.dart +29 -0
  7. package/dart/lib/src/nms_test_environment.dart +8 -0
  8. package/dart/lib/src/node_runner_transport.dart +56 -18
  9. package/dart/lib/src/provider.dart +5 -1
  10. package/dart/lib/src/record_origin.dart +55 -0
  11. package/dart/lib/src/runner/node-entry.mjs +9105 -0
  12. package/dart/lib/src/runner/offline-install.mjs +4729 -0
  13. package/dart/lib/src/session_bound.dart +23 -10
  14. package/dart/lib/src/source_trust.dart +51 -0
  15. package/dart/lib/src/test_harness.dart +490 -58
  16. package/dart/lib/src/webview_bridge.dart +167 -58
  17. package/dart/lib/src/workspace_name.dart +7 -1
  18. package/dart/pubspec.lock +3 -3
  19. package/dart/pubspec.yaml +4 -2
  20. package/dart/test/acquire_session_credentials_test.dart +34 -17
  21. package/dart/test/acquire_session_test.dart +109 -24
  22. package/dart/test/adv_realm_interference_test.dart +227 -99
  23. package/dart/test/bundle_version_pairing_test.dart +1 -1
  24. package/dart/test/cloud_bound_topology_test.dart +1 -1
  25. package/dart/test/cluster_attested_test.dart +52 -35
  26. package/dart/test/cluster_births_test.dart +5 -2
  27. package/dart/test/cluster_device_keyed_acceptance_test.dart +5 -2
  28. package/dart/test/cluster_local_autosync_test.dart +5 -2
  29. package/dart/test/cluster_scale_test.dart +5 -2
  30. package/dart/test/compat_test.dart +1 -1
  31. package/dart/test/dlq_session_surface_test.dart +2 -1
  32. package/dart/test/errors_test.dart +1 -1
  33. package/dart/test/first_app_gauntlet_test.dart +78 -51
  34. package/dart/test/four_layer_nested_topology_test.dart +1 -1
  35. package/dart/test/harness_phase_timeout_test.dart +1 -1
  36. package/dart/test/home_parity_test.dart +9 -5
  37. package/dart/test/local_sync_inert_test.dart +22 -14
  38. package/dart/test/node_runner_transport_test.dart +9 -5
  39. package/dart/test/null_slot_test.dart +1 -1
  40. package/dart/test/parent_signer_recovery_test.dart +5 -2
  41. package/dart/test/schema_validation_test.dart +1 -1
  42. package/dart/test/sealing_client_test.dart +2 -1
  43. package/dart/test/session_bound_test.dart +92 -119
  44. package/dart/test/stale_bundle_test.dart +1 -1
  45. package/dart/test/test_harness_test.dart +77 -22
  46. package/dart/test/vo_read_typing_test.dart +32 -14
  47. package/dart/test/watch_after_offer_stall_test.dart +2 -1
  48. package/dart/test/webview_bridge_test.dart +17 -4
  49. package/dart/test/wire_test.dart +1 -1
  50. package/dart_types/CHANGELOG.md +8 -0
  51. package/dart_types/lib/nomos_types.dart +7 -6
  52. package/dart_types/lib/src/activity.dart +23 -13
  53. package/dart_types/lib/src/foreign_ref.dart +61 -112
  54. package/dart_types/lib/src/invitation.dart +96 -0
  55. package/dart_types/lib/src/nomos_ref.dart +97 -59
  56. package/dart_types/pubspec.yaml +1 -1
  57. package/dart_types/test/surfaced_test.dart +125 -19
  58. package/package.json +7 -3
  59. package/runtime/generated-application-host.js +8851 -0
  60. package/src/activity.ts +2 -2
  61. package/src/aggregate.ts +24 -1
  62. package/src/application_composition.ts +282 -0
  63. package/src/application_topology.ts +151 -0
  64. package/src/attested_core.ts +2 -2
  65. package/src/authoring_context.ts +9 -0
  66. package/src/birth_meter_ensure.ts +1 -1
  67. package/src/births.ts +53 -5
  68. package/src/build_package.ts +79 -71
  69. package/src/business_birth.ts +93 -10
  70. package/src/clear_directive.ts +3 -1
  71. package/src/codegen_dart.ts +369 -420
  72. package/src/codegen_dart_application.ts +1508 -0
  73. package/src/codegen_dart_client.ts +156 -95
  74. package/src/codegen_dart_scenario.ts +299 -0
  75. package/src/codegen_openapi.ts +8 -3
  76. package/src/codegen_proof.ts +1113 -1137
  77. package/src/codegen_provider_dart.ts +12 -1
  78. package/src/codegen_ts.ts +1176 -496
  79. package/src/compat_package_main.ts +22 -10
  80. package/src/compile_package_main.ts +1536 -337
  81. package/src/contract.ts +10 -4
  82. package/src/dart_barrel_guard.ts +5 -1
  83. package/src/describe_model.ts +13 -5
  84. package/src/directive.ts +244 -5
  85. package/src/engine_entry.ts +56 -13
  86. package/src/engine_javascript.ts +31 -0
  87. package/src/evolve_preview.ts +3 -12
  88. package/src/framework/application_home.ts +24 -10
  89. package/src/framework/capability.ts +15 -1
  90. package/src/framework/domain_lifecycle.ts +8 -0
  91. package/src/framework/framework_aggregates.ts +2 -2
  92. package/src/framework/genesis.ts +11 -1
  93. package/src/framework/home.ts +178 -75
  94. package/src/framework/impure_capability.ts +24 -7
  95. package/src/framework/relations.ts +13 -5
  96. package/src/framework/workspaces.ts +13 -12
  97. package/src/generated_client_names.ts +42 -3
  98. package/src/idof_lint.ts +65 -13
  99. package/src/index.ts +46 -12
  100. package/src/intent.ts +587 -0
  101. package/src/law_check.ts +64 -16
  102. package/src/precondition.ts +16 -4
  103. package/src/read_projection_namespace.ts +2 -7
  104. package/src/recipient.ts +33 -0
  105. package/src/relation_schema.ts +93 -0
  106. package/src/secondary_ensure.ts +11 -2
  107. package/src/shape_compat.ts +11 -1
  108. package/src/usd.ts +30 -8
  109. package/src/usd_layers.ts +447 -246
  110. package/src/usd_state.ts +1 -1
  111. package/src/value_object.ts +49 -13
  112. package/src/wire_encode.ts +124 -26
  113. package/src/workspace_sharing.ts +269 -0
  114. package/src/zod_introspection.ts +84 -0
  115. package/dart/example/nomos_secure_key_store.dart +0 -82
  116. package/dart/example/nomos_webview_transport.dart +0 -63
  117. package/dart/lib/nomos_headless.dart +0 -50
  118. package/dart/lib/testing.dart +0 -14
  119. package/dart/test/co2_product_gauntlet_test.dart +0 -244
  120. package/src/framework/identity.ts +0 -537
@@ -19,17 +19,17 @@
19
19
  * - a Dart enum per `t.enum([...])` field;
20
20
  * - a Dart intent class per domain action — a pure typed DTO. Each class exposes its
21
21
  * `domain` / `intentType` (so the call site never hand-types a string) and a
22
- * `toPayloadJson()` that serialises only its typed fields to data. It builds no
22
+ * internal `nmsInternalToPayloadJson()` seam that serialises only its typed fields to data. It builds no
23
23
  * `Event` and no `Hlc`: the GitHolon executes the installed domain plan.
24
24
  * - a single generic effectful write verb [`offer`] that ships {domain, intentType,
25
- * toPayloadJson()} to the GitHolon `author(domain, intentType, payloadJson, actor)`.
25
+ * nmsInternalToPayloadJson()} to the GitHolon `author(domain, intentType, payloadJson, actor)`.
26
26
  *
27
27
  * It does NOT hand-write the fixed wire types (`Value`/`Op`/`Event`/...): those
28
28
  * live hand-written in `dart_types/lib/src/wire.dart`. Only the per-domain shapes are
29
29
  * generated, so they stay reproducible from the domain definitions.
30
30
  */
31
31
  import { z } from "zod";
32
- import { voMetaOf, collectPayloadValueObjectDescriptors, idOfMetaOf, brandedIdMetaOf, NOMOS_UNIT_BRAND } from "./value_object.js";
32
+ import { voMetaOf, collectPayloadValueObjectDescriptors, idOfMetaOf, idOfTargetKindOf, brandedIdMetaOf, NOMOS_UNIT_BRAND } from "./value_object.js";
33
33
  import { recursiveTypeBaseName, zodUnwrapLazy } from "./zod_lazy.js";
34
34
  import type { AggregateHandle } from "./aggregate.js";
35
35
  import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
@@ -53,12 +53,25 @@ import type { WorkspaceTypeDecl } from "./workspace_type.js";
53
53
  import { defaultActivityDescriptor } from "./activity.js";
54
54
  import { activityKey } from "./activity.js";
55
55
  import { BUSINESS_CORRECTION_DIRECTIVE } from "./framework/business_corrections.js";
56
+ import { isBusinessIntent } from "./intent.js";
57
+ import { contractTypeName } from "./contract.js";
58
+ import { isNamespacedAggregate, qualifyReadProjectionType } from "./read_projection_namespace.js";
59
+ import { recipientMetaOf } from "./recipient.js";
56
60
 
57
61
  /** A directive of any payload type. `Directive<P>` is invariant in `P`, so the
58
62
  * generator (which only introspects shapes) takes the `any`-payload form. */
59
63
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
64
  type AnyDirective = Directive<any>;
61
65
 
66
+ /** Public business actions accept input facts. Private directive DTOs retain their wire-oriented name. */
67
+ export function dartActionInputClassName(directive: AnyDirective): string {
68
+ const businessBirth = (directive as { businessBirth?: { aggregateId?: string } }).businessBirth;
69
+ if (businessBirth?.aggregateId !== undefined && !isBusinessIntent(directive)) {
70
+ return `Create${concreteClassName(businessBirth.aggregateId)}Input`;
71
+ }
72
+ return `${cap(directive.id)}${isBusinessIntent(directive) ? "Input" : "Payload"}`;
73
+ }
74
+
62
75
  // ---- Zod payload introspection --------------------------------------------
63
76
 
64
77
  /** How a typed payload field serialises into the payload-as-DATA JSON the engine reads. */
@@ -69,18 +82,22 @@ export type ToJsonWrap =
69
82
  | "json" // nested object — emit the field's `Map<String, Object?>` directly.
70
83
  | "vo" // a declared value object — emit `.toJson()` (the generated VO class).
71
84
  | "voList" // a list of value objects — emit `.map((e) => e.toJson()).toList()`.
72
- | "wrapped"; // a typed-id class or unit extension type — emit `.value` (the wire scalar).
85
+ | "wrapped" // a standalone branded-id or unit extension type — emit `.value`.
86
+ | "ref" // an aggregate-specific NomosRef<T> — emit its `.id` wire scalar.
87
+ | "recipient"; // a framework-created Recipient<User> — emit its private delivery address.
73
88
 
74
89
  export interface PayloadFieldSpec {
75
90
  name: string;
76
91
  dartType: string;
92
+ /** Aggregate wire type when this field is a NomosRef<T>. */
93
+ referenceType?: string;
77
94
  /** for enums: the Dart enum type name + raw values. */
78
95
  enumValues?: string[];
79
- /** how this field serialises into `toPayloadJson()`. */
96
+ /** how this field serialises into `nmsInternalToPayloadJson()`. */
80
97
  toJson: ToJsonWrap;
81
98
  /**
82
99
  * Whether the payload field is OPTIONAL (Zod `.optional()`/`.default()`). Optional
83
- * fields are nullable in Dart and OMITTED from `toPayloadJson()` when null — so the
100
+ * fields are nullable in Dart and OMITTED from `nmsInternalToPayloadJson()` when null — so the
84
101
  * engine's `.plan()` sees exactly the present-field set its TS Zod schema would (it
85
102
  * only emits ops for present fields). Required fields are always emitted.
86
103
  */
@@ -89,11 +106,67 @@ export interface PayloadFieldSpec {
89
106
  * NULL SEMANTICS (null_semantics.md): the payload field ADMITS a domain `null` (its zod is
90
107
  * `.nullable()`, a `t.nullable(...)`/`t.clearable(...)` sink). It generates as the sealed
91
108
  * tri-state [NomosSlot] so the call site says OMITTED / EXPLICIT-NULL / PRESENT explicitly —
92
- * `toPayloadJson` then omits, writes a real `null`, or writes the value accordingly.
109
+ * `nmsInternalToPayloadJson` then omits, writes a real `null`, or writes the value accordingly.
93
110
  * `false`/absent for a plain field (byte-identical to a pre-feature payload). */
94
111
  nullAdmits?: boolean;
95
112
  }
96
113
 
114
+ export interface DartBusinessRecordInput {
115
+ readonly idField: string;
116
+ readonly workspaceField: string;
117
+ readonly argument: string;
118
+ readonly dartType: string;
119
+ }
120
+
121
+ /**
122
+ * Resolve `.fromRecord(field)` against the directive's target aggregate. The
123
+ * aggregate owns the foreign-reference pairing, so authors name only the
124
+ * business id field and cannot accidentally pair it with the wrong workspace.
125
+ */
126
+ export function dartBusinessRecordInputs(
127
+ directive: AnyDirective,
128
+ aggregates: readonly AggregateHandle[],
129
+ ): DartBusinessRecordInput[] {
130
+ const declared = directive.businessRecordInputs ?? [];
131
+ if (declared.length === 0) return [];
132
+ const aggregate = aggregates.find((candidate) => candidate.id === directive.aggregateId);
133
+ if (aggregate === undefined) {
134
+ throw new Error(`${directive.id}.fromRecord(): target aggregate '${directive.aggregateId}' is not generated`);
135
+ }
136
+ const payloadNames = new Set(introspectPayload(directive.payloadSchema as z.ZodTypeAny, new Map()).map((spec) => spec.name));
137
+ const seenArguments = new Set<string>();
138
+ return declared.map((input) => {
139
+ const field = aggregate.fields[input.field] as Field | undefined;
140
+ if (field?.kind !== "ref" || field.foreignRef === undefined) {
141
+ throw new Error(
142
+ `${directive.id}.fromRecord('${input.field}'): '${directive.aggregateId}.${input.field}' must be a foreignRef field`,
143
+ );
144
+ }
145
+ const workspaceField = field.foreignRef.workspaceField;
146
+ if (!payloadNames.has(input.field) || !payloadNames.has(workspaceField)) {
147
+ throw new Error(
148
+ `${directive.id}.fromRecord('${input.field}'): payload must carry both '${input.field}' and its declared locator '${workspaceField}'`,
149
+ );
150
+ }
151
+ const fallback = input.field.endsWith("Id") ? input.field.slice(0, -2) : `${input.field}Record`;
152
+ const argument = input.as ?? fallback;
153
+ if (!/^[a-z][A-Za-z0-9_]*$/.test(argument)) {
154
+ throw new Error(`${directive.id}.fromRecord('${input.field}'): '${argument}' is not a lowerCamelCase Dart argument`);
155
+ }
156
+ if (seenArguments.has(argument) || (payloadNames.has(argument) && argument !== input.field && argument !== workspaceField)) {
157
+ throw new Error(`${directive.id}.fromRecord('${input.field}'): business argument '${argument}' collides with another payload field`);
158
+ }
159
+ seenArguments.add(argument);
160
+ const target = field.foreignRef.targetAggregateId;
161
+ return {
162
+ idField: input.field,
163
+ workspaceField,
164
+ argument,
165
+ dartType: field.foreignRef.targetIsContract === true ? contractTypeName(target) : concreteClassName(target),
166
+ };
167
+ });
168
+ }
169
+
97
170
  type ZodInternalDef = {
98
171
  type?: string | z.ZodTypeAny;
99
172
  innerType?: z.ZodTypeAny;
@@ -269,11 +342,11 @@ export function introspectPayload(
269
342
 
270
343
  /**
271
344
  * The aggregate ids a directive set references via `idOf(Agg)` in its TOP-LEVEL payload fields
272
- * (dart_codegen_parity.md P2). A cross-domain `idOf` types the field as the sibling domain's typed
273
- * id class (`ListingId`) — which lives in the OWNING domain's generated file, so the referencing
345
+ * (dart_codegen_parity.md P2). A cross-domain `idOf` types the field as `NomosRef<SiblingAggregate>`;
346
+ * the aggregate type lives in the owning domain's generated file, so the referencing
274
347
  * domain's file must import it (flat: a relative `<domain>.dart`; layered: the sibling TYPES file).
275
348
  * Returns EVERY idOf target (same- and cross-domain); the caller filters to the cross-domain ones it
276
- * must import (a same-domain target's id class is declared locally). Nullable/optional-wrapped
349
+ * must import (a same-domain target aggregate is declared locally). Nullable/optional-wrapped
277
350
  * idOf fields are unwrapped by `idOfMetaOf`.
278
351
  */
279
352
  export function collectPayloadIdOfTargets(directives: readonly { payloadSchema?: z.ZodTypeAny }[]): Set<string> {
@@ -305,6 +378,9 @@ function introspectField(
305
378
  enumDartTypes: Map<string, string>,
306
379
  lazyNames?: Map<z.ZodTypeAny, string>,
307
380
  ): Omit<PayloadFieldSpec, "optional"> {
381
+ if (recipientMetaOf(zt) !== undefined) {
382
+ return { name, dartType: "Recipient<User>", toJson: "recipient" };
383
+ }
308
384
  // RECURSIVE `z.lazy` (a self-referencing tree/JSON payload): if the compiler synthesized a
309
385
  // typed recursive class for this knot (`lazyNames`), the field IS that class (toJson via its
310
386
  // `.toJson()`). Without a synthesized class (no map, or a degraded knot) it resolves one level
@@ -334,11 +410,20 @@ function introspectField(
334
410
  const tn = zodTypeName(zt);
335
411
  switch (tn) {
336
412
  case "ZodString": {
337
- // idOf(Agg) (dart_codegen_parity.md P2): wire-identical to z.string(), but the Dart
338
- // field is the aggregate's TYPED id class — cross-aggregate id mixups fail to compile.
413
+ // idOf(Agg): wire-identical to z.string(), but the public Dart value is the ONE
414
+ // workspace-qualified aggregate reference. NomosRef<Site> and NomosRef<Asset> are
415
+ // different compile-time types; serialization keeps the kernel's string id wire.
339
416
  const idAgg = idOfMetaOf(zt);
340
417
  if (idAgg !== undefined) {
341
- return { name, dartType: typedIdClassForAggregateType(idAgg), toJson: "wrapped" };
418
+ const targetClass = idOfTargetKindOf(zt) === "contract"
419
+ ? contractTypeName(idAgg)
420
+ : concreteClassName(idAgg);
421
+ return {
422
+ name,
423
+ dartType: `NomosRef<${targetClass}>`,
424
+ referenceType: idAgg,
425
+ toJson: "ref",
426
+ };
342
427
  }
343
428
  // brandedId("SiteId"): a standalone nominal string → the generated extension type.
344
429
  const brand = brandedIdMetaOf(zt);
@@ -447,24 +532,30 @@ interface AggFieldSpec {
447
532
  kind: MaterializedFieldKind;
448
533
  dartType: string;
449
534
  enumValues?: string[];
450
- /** For an own-id string field represented as a typed aggregate id value class. */
535
+ /** For an own-id string field represented as `NomosRef<TAggregate>`. */
451
536
  valueClass?: string;
537
+ /** Stable wire type for an own-id field's canonical reference. */
538
+ valueAggregateType?: string;
452
539
  /**
453
- * For a `ref`-kind field: the TYPED id class of the target aggregate (e.g. a
454
- * `Building.siteId` ref → `SiteId`). The field's Dart type is this typed id (NOT a
540
+ * For a `ref`-kind field: the generated class of the target aggregate (e.g. a
541
+ * `Building.siteId` ref → `NomosRef<Site>`). The field's Dart type is this reference (NOT a
455
542
  * bare `String`), so a cross-aggregate ref carries its target's TYPE — passing a
456
- * `RoomId` where a `SiteId` is expected is a compile error. `undefined` for a
543
+ * `NomosRef<Room>` where a `NomosRef<Site>` is expected is a compile error. `undefined` for a
457
544
  * non-ref field (or a ref whose target type is unknown — falls back to `String`).
458
545
  */
459
546
  refIdClass?: string;
547
+ /** The target aggregate's stable wire type, carried in the runtime reference triple. */
548
+ refAggregateType?: string;
460
549
  /**
461
550
  * For a `t.foreignRef` field (a TYPED CROSS-WORKSPACE LOCATOR, not an atomic FK): the target
462
551
  * aggregate's Dart class (e.g. `CatalogueListing`). The field's Dart type is `ForeignRef<Target>`
463
- * (from the shared `nomos_types` runtime), NOT the target's typed-id class — so the app can
552
+ * (from the shared `nomos_types` runtime), rather than a same-workspace `NomosRef` — so the app can
464
553
  * `resolve`/`watch`/`attest` the foreign target on demand. `undefined` for a plain same-workspace
465
- * `ref` field (which stays its target's typed id class via {@link refIdClass}).
554
+ * `ref` field (which stays `NomosRef<Target>` via {@link refIdClass}).
466
555
  */
467
556
  foreignRefTarget?: string;
557
+ /** The target aggregate's domain-qualified kernel type carried by the locator. */
558
+ foreignRefAggregateType?: string;
468
559
  /**
469
560
  * For a `t.foreignRef` field: the wire name of the SIBLING field on THIS aggregate holding the foreign
470
561
  * WORKSPACE id (the locator's `workspace`, dynamic per row). `undefined` for a non-foreignRef field.
@@ -601,11 +692,7 @@ function dartTypeNameFromField(raw: string): string {
601
692
  }
602
693
 
603
694
  function aggregateIdDartType(aggregateType: string): string {
604
- return typedIdClassForAggregateType(aggregateType);
605
- }
606
-
607
- function aggregateIdFromRowExpr(aggregateType: string, rowIdExpr: string): string {
608
- return `${aggregateIdDartType(aggregateType)}.fromRow(${rowIdExpr})`;
695
+ return `NomosRef<${concreteClassName(aggregateType)}>`;
609
696
  }
610
697
 
611
698
  function aggregateFieldSpecForKey(
@@ -613,7 +700,7 @@ function aggregateFieldSpecForKey(
613
700
  aggregateType: string,
614
701
  keyField: string,
615
702
  enumDartTypes: Map<string, string>,
616
- refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
703
+ refIdClassForAggregate: (aggregateType: string) => string = concreteClassName,
617
704
  ownIdFieldsByAggregate: ReadonlyMap<string, ReadonlySet<string>> = new Map(),
618
705
  ): AggFieldSpec | undefined {
619
706
  const agg = aggregatesById.get(aggregateType);
@@ -621,9 +708,16 @@ function aggregateFieldSpecForKey(
621
708
  if (field === undefined || field.kind === "hasMany") return undefined;
622
709
  const valueClass =
623
710
  field.kind === "string" && (ownIdFieldsByAggregate.get(aggregateType)?.has(keyField) ?? false)
624
- ? aggregateIdDartType(aggregateType)
711
+ ? concreteClassName(aggregateType)
625
712
  : undefined;
626
- return aggFieldSpec(keyField, field, enumDartTypes, refIdClassForAggregate, valueClass);
713
+ return aggFieldSpec(
714
+ keyField,
715
+ field,
716
+ enumDartTypes,
717
+ refIdClassForAggregate,
718
+ valueClass,
719
+ valueClass === undefined ? undefined : aggregateType,
720
+ );
627
721
  }
628
722
 
629
723
  function readKeyParamSpec(
@@ -632,10 +726,10 @@ function readKeyParamSpec(
632
726
  ): { dartType: string; valueExpr: string } {
633
727
  if (fieldSpec === undefined) return { dartType: "String", valueExpr: paramName };
634
728
  if (fieldSpec.valueClass !== undefined) {
635
- return { dartType: fieldSpec.valueClass, valueExpr: `${paramName}.value` };
729
+ return { dartType: `NomosRef<${fieldSpec.valueClass}>`, valueExpr: `NmsInternalNomosRefs.id(${paramName})` };
636
730
  }
637
731
  if (fieldSpec.kind === "ref" && fieldSpec.refIdClass !== undefined) {
638
- return { dartType: fieldSpec.refIdClass, valueExpr: `${paramName}.value` };
732
+ return { dartType: `NomosRef<${fieldSpec.refIdClass}>`, valueExpr: `NmsInternalNomosRefs.id(${paramName})` };
639
733
  }
640
734
  if (fieldSpec.kind === "enum") {
641
735
  return { dartType: fieldSpec.dartType, valueExpr: `${paramName}.wire` };
@@ -658,7 +752,7 @@ function refIdClassResolver(mod: DomainModule): (aggregateType: string) => strin
658
752
  }
659
753
  return (aggregateType: string): string => {
660
754
  const imp = imports.get(aggregateType);
661
- const cls = typedIdClassForAggregateType(aggregateType);
755
+ const cls = concreteClassName(aggregateType);
662
756
  return imp === undefined ? cls : `${imp.prefix}.${cls}`;
663
757
  };
664
758
  }
@@ -736,8 +830,10 @@ function aggFieldSpec(
736
830
  wireName: string,
737
831
  field: Field,
738
832
  enumDartTypes: Map<string, string>,
739
- refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
833
+ refIdClassForAggregate: (aggregateType: string) => string = concreteClassName,
740
834
  valueClass?: string,
835
+ valueAggregateType?: string,
836
+ aggregateWireTypeForAggregate: (aggregateType: string) => string = (aggregateType) => aggregateType,
741
837
  ): AggFieldSpec {
742
838
  const name = dartMemberName(wireName);
743
839
  // Collections are ALWAYS empty-defaultable; scalars are optional only when the DSL
@@ -749,9 +845,11 @@ function aggFieldSpec(
749
845
  name,
750
846
  wireName,
751
847
  kind: field.kind,
752
- dartType: valueClass ?? "String",
848
+ dartType: valueClass === undefined ? "String" : `NomosRef<${valueClass}>`,
753
849
  optional,
754
- ...(valueClass !== undefined ? { valueClass } : {}),
850
+ ...(valueClass !== undefined && valueAggregateType !== undefined
851
+ ? { valueClass, valueAggregateType }
852
+ : {}),
755
853
  };
756
854
  case "json":
757
855
  // A NAMED VALUE OBJECT (`ValueObject.field()`) types as its generated Dart class
@@ -786,7 +884,7 @@ function aggFieldSpec(
786
884
  // A contract target names the shared DTO class `<ContractId>Contract` (emitted into the shared
787
885
  // `contracts.dart`); the aggregate class is `<ContractId>` and must not collide, hence the suffix.
788
886
  const target = field.foreignRef.targetIsContract === true
789
- ? `${field.foreignRef.targetAggregateId}Contract`
887
+ ? contractTypeName(field.foreignRef.targetAggregateId)
790
888
  : concreteClassName(field.foreignRef.targetAggregateId);
791
889
  return {
792
890
  name,
@@ -795,12 +893,14 @@ function aggFieldSpec(
795
893
  dartType: `ForeignRef<${target}>`,
796
894
  optional,
797
895
  foreignRefTarget: target,
896
+ foreignRefAggregateType: aggregateWireTypeForAggregate(
897
+ field.foreignRef.targetAggregateId,
898
+ ),
798
899
  foreignRefWorkspaceField: field.foreignRef.workspaceField,
799
900
  };
800
901
  }
801
- // A plain same-workspace `ref` field carries ANOTHER aggregate's id — type it as that target's
802
- // TYPED id class (cross-aggregate ref typing), so e.g. a `Building.siteId`
803
- // ref is a `SiteId`, never a bare String. The target wire id is recorded on
902
+ // A plain same-workspace `ref` field carries ANOTHER aggregate's identity — type it as
903
+ // `NomosRef<Target>` (cross-aggregate ref typing), never a bare String. The target wire id is recorded on
804
904
  // the field (`refAggregateId`); fall back to `String` only if it is absent.
805
905
  const targetType = field.refAggregateId;
806
906
  const refIdClass =
@@ -811,9 +911,14 @@ function aggFieldSpec(
811
911
  name,
812
912
  wireName,
813
913
  kind: field.kind,
814
- dartType: refIdClass ?? "String",
914
+ dartType: refIdClass === undefined ? "String" : `NomosRef<${refIdClass}>`,
815
915
  optional,
816
- ...(refIdClass !== undefined ? { refIdClass } : {}),
916
+ ...(refIdClass !== undefined && targetType !== undefined
917
+ ? {
918
+ refIdClass,
919
+ refAggregateType: aggregateWireTypeForAggregate(targetType),
920
+ }
921
+ : {}),
817
922
  };
818
923
  }
819
924
  case "int":
@@ -824,7 +929,16 @@ function aggFieldSpec(
824
929
  case "counter":
825
930
  return { name, wireName, kind: "int", dartType: "int", optional };
826
931
  case "position":
827
- return { name, wireName, kind: "string", dartType: valueClass ?? "String", optional, ...(valueClass !== undefined ? { valueClass } : {}) };
932
+ return {
933
+ name,
934
+ wireName,
935
+ kind: "string",
936
+ dartType: valueClass === undefined ? "String" : `NomosRef<${valueClass}>`,
937
+ optional,
938
+ ...(valueClass !== undefined && valueAggregateType !== undefined
939
+ ? { valueClass, valueAggregateType }
940
+ : {}),
941
+ };
828
942
  case "bool":
829
943
  return { name, wireName, kind: field.kind, dartType: "bool", optional };
830
944
  case "enum": {
@@ -872,10 +986,11 @@ function aggFieldSpec(
872
986
  function aggregateFieldSpecs(
873
987
  agg: AggregateHandle,
874
988
  enumDartTypes: Map<string, string>,
875
- refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
989
+ refIdClassForAggregate: (aggregateType: string) => string = concreteClassName,
876
990
  ownIdFields: ReadonlySet<string> = new Set(),
991
+ aggregateWireTypeForAggregate: (aggregateType: string) => string = (aggregateType) => aggregateType,
877
992
  ): AggFieldSpec[] {
878
- const used = new Set<string>(["aggregateId"]);
993
+ const used = new Set<string>(["ref"]);
879
994
  const internal = new Set(agg.internalFields ?? []);
880
995
  // `agg.fields` is STORED fields only — virtual `t.hasMany` inverses are in `agg.hasMany`
881
996
  // and never reach Dart materialization (they are served by the inverse read index).
@@ -884,7 +999,7 @@ function aggregateFieldSpecs(
884
999
  .map(([name, f]) => {
885
1000
  const valueClass =
886
1001
  f.kind === "string" && ownIdFields.has(name)
887
- ? aggregateIdDartType(agg.id)
1002
+ ? concreteClassName(agg.id)
888
1003
  : undefined;
889
1004
  const base = aggFieldSpec(
890
1005
  name,
@@ -892,6 +1007,10 @@ function aggregateFieldSpecs(
892
1007
  enumDartTypes,
893
1008
  refIdClassForAggregate,
894
1009
  valueClass,
1010
+ valueClass === undefined
1011
+ ? undefined
1012
+ : aggregateWireTypeForAggregate(agg.id),
1013
+ aggregateWireTypeForAggregate,
895
1014
  );
896
1015
  // PARTIAL-FOLD DOCTRINE (every read-model field is nullable). The projection
897
1016
  // serves PARTIAL folds: a freshly-created or not-yet-fully-folded aggregate may
@@ -923,6 +1042,12 @@ function aggregateFieldSpecs(
923
1042
  ? { ...surfacedSpec, nullAdmits: true, ...(nmeta.nullReason !== undefined ? { nullReason: nmeta.nullReason } : {}) }
924
1043
  : surfacedSpec;
925
1044
  if (used.has(spec.name)) {
1045
+ if (spec.name === "ref") {
1046
+ throw new Error(
1047
+ `aggregate '${agg.id}' field '${spec.wireName}' collides with Nomos's canonical record reference 'ref' — ` +
1048
+ `rename the business field; NomosRef<${concreteClassName(agg.id)}> is the one generated identity for this aggregate`,
1049
+ );
1050
+ }
926
1051
  throw new Error(
927
1052
  `aggregate '${agg.id}' field '${spec.wireName}' maps to duplicate Dart field '${spec.name}'`,
928
1053
  );
@@ -938,21 +1063,9 @@ export function cap(s: string): string {
938
1063
  return s.charAt(0).toUpperCase() + s.slice(1);
939
1064
  }
940
1065
 
941
- // ---- typed-id naming (the type-safety keystone, generalized) ----------------
942
- //
943
- // A DISTINCT typed id class is emitted PER AGGREGATE so passing one aggregate's
944
- // id where another's is expected is a COMPILE ERROR. The class NAME must be a pure
945
- // function of the aggregate's wire TYPE (e.g. `SiteRootAggregate`) so three sites
946
- // agree on it everywhere it is referenced — the typed-id class declaration, every
947
- // `createX` factory's return type, AND every cross-aggregate REF field that targets
948
- // that type. Convention: strip a trailing `RootAggregate`/`Aggregate` suffix and
949
- // append `Id` — `SiteRootAggregate → SiteId`, `ThingAggregate → ThingId`,
950
- // `TrackableThing → TrackableThingId`, `BuildingAggregate → BuildingId`. The
951
- // `RootAggregate` strip BEFORE `Aggregate` keeps the proven `SiteId` byte-identical.
952
-
953
1066
  // ---- Dart product-name registry (`.dartName`, architecture/dart_naming.md) ----
954
1067
  // A wire id → declared Dart base name map. When an aggregate declared `.dartName("Site")`,
955
- // EVERY Dart naming derivation (concrete class, `<stem>ReadModel`, `<stem>Id`,
1068
+ // EVERY Dart naming derivation (concrete class, `<stem>ReadModel`, `NomosRef<Concrete>`,
956
1069
  // `read/watch<Name>ById`, `ForeignRef<Target>`) consults this instead of stemming the wire id.
957
1070
  // Populated per-domain by `registerDartNames` at each Dart-emit entry point. NON-law, Dart-only
958
1071
  // (TS codegen never reads it); empty ⇒ every derivation is byte-identical to before this existed.
@@ -1002,24 +1115,6 @@ function dartNameStem(aggregateType: string): string {
1002
1115
  return cap(stem === "" ? aggregateType : stem);
1003
1116
  }
1004
1117
 
1005
- /** The typed-id class name for an aggregate WIRE TYPE (e.g. `SiteRootAggregate` →
1006
- * `SiteId`). Pure + deterministic: the SAME wire type ALWAYS yields the SAME name,
1007
- * so the id-class decl, the `createX` factory return, and every typed ref field that
1008
- * points at this type all agree. Honours `.dartName` (`Site` → `SiteId`). */
1009
- export function typedIdClassForAggregateType(aggregateType: string): string {
1010
- const declared = dartNameByWireId.get(aggregateType);
1011
- if (declared !== undefined) return `${declared}Id`;
1012
- let stem = aggregateType;
1013
- if (stem.endsWith("RootAggregate")) {
1014
- stem = stem.slice(0, -"RootAggregate".length);
1015
- } else if (stem.endsWith("Aggregate")) {
1016
- stem = stem.slice(0, -"Aggregate".length);
1017
- }
1018
- // Empty stem (a type literally named `Aggregate`) is impossible for registered
1019
- // aggregates, but guard so the name is always a legal identifier.
1020
- return `${cap(stem === "" ? aggregateType : stem)}Id`;
1021
- }
1022
-
1023
1118
  /** Collect all enum value-sets across aggregates + directives, name each ONCE. */
1024
1119
  function collectEnums(
1025
1120
  aggregates: AggregateHandle[],
@@ -1271,16 +1366,17 @@ function emitPermissionVocabulary(vocab: PermissionVocabulary | undefined): stri
1271
1366
  function emitAggregate(
1272
1367
  agg: AggregateHandle,
1273
1368
  enumDartTypes: Map<string, string>,
1274
- refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
1369
+ refIdClassForAggregate: (aggregateType: string) => string = concreteClassName,
1275
1370
  ownIdFields: ReadonlySet<string> = new Set(),
1371
+ aggregateWireTypeForAggregate: (aggregateType: string) => string = (aggregateType) => aggregateType,
1276
1372
  ): string {
1277
1373
  const className = concreteClassName(agg.id); // wire id (already PascalCase) unless `.dartName` overrides.
1278
- const idType = aggregateIdDartType(agg.id);
1279
1374
  const specs = aggregateFieldSpecs(
1280
1375
  agg,
1281
1376
  enumDartTypes,
1282
1377
  refIdClassForAggregate,
1283
1378
  ownIdFields,
1379
+ aggregateWireTypeForAggregate,
1284
1380
  );
1285
1381
  // ATTRIBUTED READS (field_provenance.md): the `.attributed()` field names — drives the
1286
1382
  // `$provenance` map + per-field `<field>Provenance` getters + `lastAuthors`. Empty for
@@ -1300,14 +1396,9 @@ function emitAggregate(
1300
1396
  // nullable (`String?`/`int?`/`Enum?`); collections (`set`/`map`) keep their non-null
1301
1397
  // container type but default to EMPTY when absent.
1302
1398
  const fieldDecls = [
1303
- ` /// The kernel aggregate id (the \`{type,id,data}\` row's \`id\`) — the id the`,
1304
- ` /// indexed/by-id accessor matched on (e.g. the siteId). Empty string only when`,
1305
- ` /// the aggregate is built from a bare \`data\` map with no row id (e.g. a unit test).`,
1306
- ` final ${idType} aggregateId;`,
1307
- ` /// THE ONE REFERENCE (architecture/nomos_ref.md): this instance's canonical, workspace-qualified`,
1308
- ` /// [NomosRef] (\`nomos://<workspace>/<type>/<id>\`) — composed at decode from the session workspace +`,
1309
- ` /// the row's type/id. Workspace is '' when decoded off the bare bridge (the client accessors fill it).`,
1310
- ` final NomosRef ref;`,
1399
+ ` /// The canonical workspace-qualified identity of this ${className}. Pass this exact value`,
1400
+ ` /// to generated actions, find and watch methods; never copy its id into application state.`,
1401
+ ` final NomosRef<${className}> ref;`,
1311
1402
  ...(attributedFields.length > 0
1312
1403
  ? [
1313
1404
  ` /// ATTRIBUTED READS (field_provenance.md): per-field author provenance for the`,
@@ -1335,8 +1426,7 @@ function emitAggregate(
1335
1426
  }),
1336
1427
  ].join("\n");
1337
1428
  const ctorParams = [
1338
- ` this.aggregateId = const ${idType}.fromRow(''),`,
1339
- ` this.ref = const NomosRef(workspace: '', type: '${agg.id}', id: ''),`,
1429
+ ` required this.ref,`,
1340
1430
  ...(attributedFields.length > 0 ? [` this.\$provenance = const {},`] : []),
1341
1431
  ...specs.map(
1342
1432
  // A required scalar stays `required`; an optional scalar OR a collection is not
@@ -1358,8 +1448,7 @@ function emitAggregate(
1358
1448
  // flat-projection reader from `subscriptions.dart`. The kernel aggregate [id] is
1359
1449
  // threaded from the row id (GAP 2) — defaulting to '' for a bare-map call.
1360
1450
  const parseLines = [
1361
- ` aggregateId: ${aggregateIdFromRowExpr(agg.id, "id")},`,
1362
- ` ref: NomosRef(workspace: workspace, type: type.isEmpty ? '${agg.id}' : type, id: id),`,
1451
+ ` ref: NmsInternalNomosRefs.create<${className}>(workspace: _location, type: _kind.isEmpty ? '${aggregateWireTypeForAggregate(agg.id)}' : _kind, id: id),`,
1363
1452
  ...(attributedFields.length > 0
1364
1453
  ? [` \$provenance: FieldProvenance.mapFromJson(data['__prov']),`]
1365
1454
  : []),
@@ -1376,7 +1465,7 @@ function emitAggregate(
1376
1465
  // distinct aggregates). An optional scalar compares with `==` (null-safe); a
1377
1466
  // collection compares set/map-wise (always non-null after the empty default).
1378
1467
  const eqLines = [
1379
- `other.aggregateId == aggregateId`,
1468
+ `other.ref == ref`,
1380
1469
  ...specs.map((s) =>
1381
1470
  s.kind === "set"
1382
1471
  ? `other.${s.name}.length == ${s.name}.length && other.${s.name}.containsAll(${s.name})`
@@ -1387,7 +1476,7 @@ function emitAggregate(
1387
1476
  ].join(" &&\n ");
1388
1477
 
1389
1478
  const hashLines = [
1390
- ` aggregateId,`,
1479
+ ` ref,`,
1391
1480
  ...specs.map((s) =>
1392
1481
  s.kind === "set" || s.kind === "map"
1393
1482
  ? ` Object.hashAll(${s.name}${s.kind === "map" ? ".entries.map((e) => Object.hash(e.key, e.value))" : ""}),`
@@ -1401,7 +1490,7 @@ function emitAggregate(
1401
1490
  `/// Deserialized via [${className}.fromJson] from the FLAT, already-decoded`,
1402
1491
  `/// projection \`data\` map (the \`{type,id,data}\` row \`query()\`/\`query_by_id()\``,
1403
1492
  `/// emits) — e.g. \`{"name":"HQ","createdAt":1000}\`. The kernel aggregate`,
1404
- `/// [aggregateId] is carried from the row \`id\`. Read it reactively through the`,
1493
+ `/// [ref] is composed from the row's workspace, type and id. Read it reactively through the`,
1405
1494
  `/// generated \`watch…\` accessors below. The projection serves PARTIAL folds, so`,
1406
1495
  `/// EVERY scalar field is nullable — \`null\` when a freshly-folded aggregate has not`,
1407
1496
  `/// yet folded it (never a strict cast that throws); collections default to EMPTY.`,
@@ -1417,10 +1506,7 @@ function emitAggregate(
1417
1506
  ` /// decoded to bare JSON), carrying the kernel aggregate [id] from the row \`id\``,
1418
1507
  ` /// (defaults to '' for a bare-map call). PARTIAL folds: every scalar decodes`,
1419
1508
  ` /// \`null\` when absent (never throws); absent collections decode empty.`,
1420
- ` ///`,
1421
- ` /// [workspace] + [type] compose the canonical [ref] (both optional — the client accessors thread the`,
1422
- ` /// session workspace + the row type; a bare decode leaves workspace '' and falls back to the type '${agg.id}').`,
1423
- ` factory ${className}.fromJson(Map<String, dynamic> data, [String id = '', String workspace = '', String type = '']) {`,
1509
+ ` factory ${className}.fromJson(Map<String, dynamic> data, [String id = '', String _location = '', String _kind = '']) {`,
1424
1510
  ` return ${className}(`,
1425
1511
  parseLines,
1426
1512
  ` );`,
@@ -1620,7 +1706,9 @@ function unionArmClassName(unionName: string, armKey: string): string {
1620
1706
  function dartTypeOfVoField(f: ValueObjectFieldDescriptor, voName: string, fieldName: string): string {
1621
1707
  switch (f.type) {
1622
1708
  case "string":
1623
- return f.brand ?? "String"; // a brandedId(...) → the generated extension-type wrapper
1709
+ return f.idOf !== undefined
1710
+ ? `NomosRef<${concreteClassName(f.idOf)}>`
1711
+ : f.brand ?? "String"; // a brandedId(...) → the generated extension-type wrapper
1624
1712
  case "number":
1625
1713
  // a UNIT-tagged number is the generated zero-cost extension type; `.int()` is Dart int.
1626
1714
  if (f.unit !== undefined) return f.unit;
@@ -1653,6 +1741,10 @@ function voFromExpr(
1653
1741
  ): string {
1654
1742
  switch (f.type) {
1655
1743
  case "string":
1744
+ if (f.idOf !== undefined) {
1745
+ const target = concreteClassName(f.idOf);
1746
+ return `NmsInternalNomosRefs.create<${target}>(workspace: '', type: '${f.idOf}', id: ${expr} as String)`;
1747
+ }
1656
1748
  return f.brand !== undefined ? `${f.brand}(${expr} as String)` : `${expr} as String`;
1657
1749
  case "enum":
1658
1750
  // TOLERANT typed decode — an unknown wire value becomes the sentinel case.
@@ -1684,6 +1776,7 @@ function voToJsonExpr(f: ValueObjectFieldDescriptor, expr: string, depth = 0): s
1684
1776
  case "number":
1685
1777
  return f.unit !== undefined ? `${expr}.value` : expr;
1686
1778
  case "string":
1779
+ if (f.idOf !== undefined) return `NmsInternalNomosRefs.id(${expr})`;
1687
1780
  return f.brand !== undefined ? `${expr}.value` : expr; // a brandedId extension type → its wire string
1688
1781
  case "boolean":
1689
1782
  case "literal":
@@ -2113,7 +2206,7 @@ function contractFromExpr(field: Field, wire: string): string {
2113
2206
  * references only nomos_types primitives (+ any VO class, which is shared). Emitted into `contracts.dart`.
2114
2207
  */
2115
2208
  function emitDartContractClass(c: import("./contract.js").ContractHandle): string {
2116
- const cls = `${c.id}Contract`;
2209
+ const cls = contractTypeName(c.id);
2117
2210
  const fields = Object.entries(c.fields).map(([wireName, f]) => ({
2118
2211
  wireName,
2119
2212
  member: dartMemberName(wireName),
@@ -2154,37 +2247,19 @@ export function generateDartContracts(
2154
2247
  * `ReferenceCode.field()`) render inside the DTOs, and their classes are HOISTED there (layer-zero),
2155
2248
  * so contracts.dart must import it (co2 #117b). */
2156
2249
  readonly frameworkImportUri?: string;
2157
- /** Local aggregates' typed-id class names — a contract named after a local aggregate must NOT
2158
- * re-declare its `<X>Id` (the aggregate's own class, with `.fromRow`, already exists). */
2159
- readonly aggregateIdClassNames?: ReadonlySet<string>;
2160
2250
  } = {},
2161
2251
  ): string {
2162
2252
  const sorted = [...contracts].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
2163
- // CONTRACT TYPED IDS (co2 #117a): `idOf(SomeContract)` in a payload types the field `<ContractId>Id` —
2164
- // exactly like an aggregate's typed id — but a contract has no aggregate emitter to declare the class.
2165
- // Emit each contract's id as a ZERO-COST String extension type here (the shared layer every domain
2166
- // imports): a const ctor from the wire string + `.value` — the exact surface the generated payloads use.
2167
- const idTypes = sorted
2168
- .map((c) => typedIdClassForAggregateType(c.id))
2169
- .filter((cls) => !(opts.aggregateIdClassNames?.has(cls) ?? false))
2170
- .map((cls) => [
2171
- `/// TYPED CONTRACT ID \`${cls}\` — a ZERO-COST nominal String (Dart extension type): the id of the`,
2172
- `/// foreign aggregate behind the contract, as \`idOf(<Contract>)\` payload fields carry it. Construct`,
2173
- `/// it from a \`ForeignRef.id\` / a queried row id: \`${cls}(ref.id)\`; \`.value\` is the wire string.`,
2174
- `extension type const ${cls}(String value) {}`,
2175
- ].join("\n"));
2176
2253
  return [
2177
2254
  `// GENERATED by nomos-compile — the package's shared domain CONTRACTS (public DTOs). DO NOT EDIT.`,
2178
2255
  `// Every domain depends on this shared layer; no domain imports another's concrete types (the acyclic graph).`,
2179
2256
  `//`,
2180
- `// ignore_for_file: unused_import`,
2257
+ `// ignore_for_file: unused_import, invalid_use_of_internal_member`,
2181
2258
  `library;`,
2182
2259
  ``,
2183
- `import 'package:nomos_client/nomos_client.dart';`,
2260
+ `import 'package:nomos_client/src/nms_generated_runtime.dart';`,
2184
2261
  ...(opts.frameworkImportUri !== undefined ? [`import '${opts.frameworkImportUri}';`] : []),
2185
2262
  ``,
2186
- ...idTypes,
2187
- ``,
2188
2263
  ...sorted.map(emitDartContractClass),
2189
2264
  ``,
2190
2265
  ].join("\n");
@@ -2213,7 +2288,18 @@ export function collectActivityDescriptors(modules: readonly DomainModule[]): Pa
2213
2288
  const out: PackageActivityEntry[] = [];
2214
2289
  for (const mod of modules) {
2215
2290
  const domain = mod.domain ?? mod.name;
2216
- for (const d of mod.directives as AnyDirective[]) {
2291
+ const privateAggregateIds = new Set(
2292
+ mod.aggregates
2293
+ .filter((aggregate) => aggregate.framework === true || (aggregate as { businessBirthInternal?: unknown }).businessBirthInternal === true)
2294
+ .map((aggregate) => aggregate.id),
2295
+ );
2296
+ const storyDirectives = mod.applicationIntents === undefined
2297
+ ? mod.directives
2298
+ : [
2299
+ ...mod.applicationIntents,
2300
+ ...mod.directives.filter((d) => d.id === BUSINESS_CORRECTION_DIRECTIVE),
2301
+ ];
2302
+ for (const d of storyDirectives as AnyDirective[]) {
2217
2303
  if ((d as { businessBirthInternal?: unknown }).businessBirthInternal === true) {
2218
2304
  if (d.id === BUSINESS_CORRECTION_DIRECTIVE) {
2219
2305
  const kind = `${domain}.correction`;
@@ -2244,7 +2330,7 @@ export function collectActivityDescriptors(modules: readonly DomainModule[]): Pa
2244
2330
  const outputs = [
2245
2331
  { marker: d.marker, aggregateId: d.aggregateId },
2246
2332
  ...(d.secondaryOutputs ?? []),
2247
- ];
2333
+ ].filter((output) => !privateAggregateIds.has(output.aggregateId));
2248
2334
  const seen = new Set<string>();
2249
2335
  const refs: Array<{ name: string; type: string; from: string }> = [];
2250
2336
  for (const output of outputs) {
@@ -2260,12 +2346,14 @@ export function collectActivityDescriptors(modules: readonly DomainModule[]): Pa
2260
2346
  out.push({
2261
2347
  domain,
2262
2348
  directiveId: d.id,
2263
- desc: desc ?? defaultActivityDescriptor({
2264
- domain,
2265
- directiveId: d.id,
2266
- ...(d.description === undefined ? {} : { description: d.description }),
2267
- refs,
2268
- }),
2349
+ desc: desc === undefined
2350
+ ? defaultActivityDescriptor({
2351
+ domain,
2352
+ directiveId: d.id,
2353
+ ...(d.description === undefined ? {} : { description: d.description }),
2354
+ refs,
2355
+ })
2356
+ : { ...desc, refs: desc.refs.filter((ref) => !privateAggregateIds.has(ref.type)) },
2269
2357
  declared: desc !== undefined,
2270
2358
  });
2271
2359
  }
@@ -2308,7 +2396,7 @@ function emitDartActivityArm(e: PackageActivityEntry): string[] {
2308
2396
  ` return ActivityEvent(`,
2309
2397
  ` eventId: ${e.correction === true ? "_activityOpaqueCorrectionId((rawRow['intentId'] ?? '') as String)" : "(rawRow['intentId'] ?? '') as String"},`,
2310
2398
  ` kind: ${dartLit(desc.kind)},`,
2311
- ` actorSubject: actorSubject,`,
2399
+ ` performedBy: actorSubject,`,
2312
2400
  ` occurredAt: (rawRow['occurredAt'] ?? '') as String,`,
2313
2401
  ` title: renderActivityTemplate(${dartLit(desc.title)}, vars),`,
2314
2402
  ` message: renderActivityTemplate(${dartLit(desc.message)}, vars),`,
@@ -2349,10 +2437,10 @@ export function generateDartActivityRegistry(entries: readonly PackageActivityEn
2349
2437
  `//`,
2350
2438
  `// Some baseline-only registries need neither payload interpolation nor one of the aggregate lists.`,
2351
2439
  `// Keep generated packages analyzer-clean without branching the shared renderer shape per directive mix.`,
2352
- `// ignore_for_file: unused_import, unused_element, unused_local_variable`,
2440
+ `// ignore_for_file: unused_import, unused_element, unused_local_variable, invalid_use_of_internal_member`,
2353
2441
  `library;`,
2354
2442
  ``,
2355
- `import 'package:nomos_client/nomos_client.dart';`,
2443
+ `import 'package:nomos_client/src/nms_generated_runtime.dart';`,
2356
2444
  ``,
2357
2445
  `/// One business-readable difference found while proving a candidate model against history.`,
2358
2446
  `enum NomosModelDifferenceKind { rules, outcome, evidence }`,
@@ -2456,7 +2544,7 @@ export function generateDartActivityRegistry(entries: readonly PackageActivityEn
2456
2544
  ` final String label;`,
2457
2545
  ` final String summary;`,
2458
2546
  ` final Map<String, dynamic>? details;`,
2459
- ` final List<NomosRef> refs;`,
2547
+ ` final List<NomosRef<Object?>> refs;`,
2460
2548
  ` const NomosDescribeItem({required this.kind, required this.id, required this.label, required this.summary, this.details, this.refs = const []});`,
2461
2549
  `}`,
2462
2550
  ``,
@@ -2665,7 +2753,7 @@ function foreignRefExpr(s: AggFieldSpec, aggregate: string): string {
2665
2753
  // workspace resolves to not-found, never throws).
2666
2754
  const wsExpr = `(readStrOrNull(data, '${wsField}') ?? '')`;
2667
2755
  const build = (idVar: string): string =>
2668
- `ForeignRef<${target}>(id: ${idVar}, workspace: ${wsExpr}, type: '${target}', fromJson: ${target}.fromJson)`;
2756
+ `NmsInternalForeignRefs.create<${target}>(id: ${idVar}, workspace: ${wsExpr}, type: '${s.foreignRefAggregateType ?? target}', fromJson: ${target}.fromJson)`;
2669
2757
  return s.optional
2670
2758
  ? `() { final v = ${idReader}; return v == null ? null : ${build("v")}; }()`
2671
2759
  : build(idReader);
@@ -2680,7 +2768,7 @@ function parseField(s: AggFieldSpec, aggregate: string): string {
2680
2768
  switch (s.kind) {
2681
2769
  case "ref": {
2682
2770
  // A `t.foreignRef` field decodes to the shared `ForeignRef<Target>` locator (raw id + sibling
2683
- // workspace field + the target `fromJson`), not a typed-id class.
2771
+ // workspace field + the target `fromJson`), not a same-workspace NomosRef.
2684
2772
  if (s.foreignRefTarget !== undefined) return foreignRefExpr(s, aggregate);
2685
2773
  // A ref reads its id STRING, then wraps it via `.fromRow` (NOT the public
2686
2774
  // `.fromMinted`, which asserts the type tag). A ref may
@@ -2692,8 +2780,8 @@ function parseField(s: AggFieldSpec, aggregate: string): string {
2692
2780
  : `readStr(data, '${aggregate}', '${s.wireName}')`;
2693
2781
  if (s.refIdClass === undefined) return reader;
2694
2782
  return opt
2695
- ? `() { final v = ${reader}; return v == null ? null : ${s.refIdClass}.fromRow(v); }()`
2696
- : `${s.refIdClass}.fromRow(${reader})`;
2783
+ ? `() { final v = ${reader}; return v == null ? null : NmsInternalNomosRefs.create<${s.refIdClass}>(workspace: _location, type: '${s.refAggregateType}', id: v); }()`
2784
+ : `NmsInternalNomosRefs.create<${s.refIdClass}>(workspace: _location, type: '${s.refAggregateType}', id: ${reader})`;
2697
2785
  }
2698
2786
  // `position` normalizes to `string`, `counter` to `int` in `aggFieldSpec` — unreachable here.
2699
2787
  case "position":
@@ -2703,8 +2791,8 @@ function parseField(s: AggFieldSpec, aggregate: string): string {
2703
2791
  ? `readStrOrNull(data, '${s.wireName}')`
2704
2792
  : `readStr(data, '${aggregate}', '${s.wireName}')`;
2705
2793
  return opt
2706
- ? `() { final v = ${reader}; return v == null ? null : ${s.valueClass}.fromRow(v); }()`
2707
- : `${s.valueClass}.fromRow(${reader})`;
2794
+ ? `() { final v = ${reader}; return v == null ? null : NmsInternalNomosRefs.create<${s.valueClass}>(workspace: _location, type: '${s.valueAggregateType}', id: v); }()`
2795
+ : `NmsInternalNomosRefs.create<${s.valueClass}>(workspace: _location, type: '${s.valueAggregateType}', id: ${reader})`;
2708
2796
  }
2709
2797
  return opt
2710
2798
  ? `readStrOrNull(data, '${s.wireName}')`
@@ -2772,15 +2860,15 @@ function slotDecodeCallback(s: AggFieldSpec, mode: "aggregate" | "readModel"): s
2772
2860
  // read through the normal field path). A plain ref keeps its typed-id decode.
2773
2861
  if (s.foreignRefTarget !== undefined) {
2774
2862
  const t = s.foreignRefTarget;
2775
- return `(v) => ForeignRef<${t}>(id: v as String, workspace: '', type: '${t}', fromJson: ${t}.fromJson)`;
2863
+ return `(v) => NmsInternalForeignRefs.create<${t}>(id: v as String, workspace: '', type: '${t}', fromJson: ${t}.fromJson)`;
2776
2864
  }
2777
2865
  return s.refIdClass !== undefined
2778
- ? `(v) => ${s.refIdClass}.fromRow(v as String)`
2866
+ ? `(v) => NmsInternalNomosRefs.create<${s.refIdClass}>(workspace: _location, type: '${s.refAggregateType}', id: v as String)`
2779
2867
  : `(v) => v as String`;
2780
2868
  case "position":
2781
2869
  case "string":
2782
2870
  return s.valueClass !== undefined
2783
- ? `(v) => ${s.valueClass}.fromRow(v as String)`
2871
+ ? `(v) => NmsInternalNomosRefs.create<${s.valueClass}>(workspace: _location, type: '${s.valueAggregateType}', id: v as String)`
2784
2872
  : `(v) => v as String`;
2785
2873
  case "json":
2786
2874
  if (s.voClass !== undefined) {
@@ -2832,9 +2920,9 @@ function readModelDartType(s: AggFieldSpec): string {
2832
2920
  switch (s.kind) {
2833
2921
  case "ref":
2834
2922
  // A `t.foreignRef` field surfaces as the typed cross-workspace `ForeignRef<Target>` locator; a plain
2835
- // same-workspace ref stays its target's typed-id class (or a bare String when the target is unknown).
2923
+ // same-workspace ref stays `NomosRef<Target>` (or a bare String when the target is unknown).
2836
2924
  if (s.foreignRefTarget !== undefined) return `ForeignRef<${s.foreignRefTarget}>`;
2837
- return s.refIdClass ?? "String";
2925
+ return s.refIdClass === undefined ? "String" : `NomosRef<${s.refIdClass}>`;
2838
2926
  case "json":
2839
2927
  // A `t.jsonObject()` leaf (`jsonShape:"object"`, e.g. `location`) is the DECODED
2840
2928
  // structured JSON OBJECT (`Map<String, Object?>`, strict). A plain `t.json()` leaf
@@ -2848,7 +2936,7 @@ function readModelDartType(s: AggFieldSpec): string {
2848
2936
  // so these labels are unreachable at runtime — present for FieldKind exhaustiveness.
2849
2937
  case "position":
2850
2938
  case "string":
2851
- return s.valueClass ?? "String";
2939
+ return s.valueClass === undefined ? "String" : `NomosRef<${s.valueClass}>`;
2852
2940
  case "counter":
2853
2941
  case "int":
2854
2942
  return "int";
@@ -2897,15 +2985,15 @@ function parseReadModelField(s: AggFieldSpec, aggregate: string): string {
2897
2985
  switch (s.kind) {
2898
2986
  case "ref": {
2899
2987
  // A `t.foreignRef` field decodes to the shared `ForeignRef<Target>` locator (same as the aggregate
2900
- // class); a plain same-workspace ref stays its target's typed-id class.
2988
+ // class); a plain same-workspace ref stays `NomosRef<Target>`.
2901
2989
  if (s.foreignRefTarget !== undefined) return foreignRefExpr(s, aggregate);
2902
2990
  const reader = opt
2903
2991
  ? `readStrOrNull(data, '${s.wireName}')`
2904
2992
  : `readStr(data, '${aggregate}', '${s.wireName}')`;
2905
2993
  if (s.refIdClass === undefined) return reader;
2906
2994
  return opt
2907
- ? `() { final v = ${reader}; return v == null ? null : ${s.refIdClass}.fromRow(v); }()`
2908
- : `${s.refIdClass}.fromRow(${reader})`;
2995
+ ? `() { final v = ${reader}; return v == null ? null : NmsInternalNomosRefs.create<${s.refIdClass}>(workspace: _location, type: '${s.refAggregateType}', id: v); }()`
2996
+ : `NmsInternalNomosRefs.create<${s.refIdClass}>(workspace: _location, type: '${s.refAggregateType}', id: ${reader})`;
2909
2997
  }
2910
2998
  case "position":
2911
2999
  case "string":
@@ -2914,8 +3002,8 @@ function parseReadModelField(s: AggFieldSpec, aggregate: string): string {
2914
3002
  ? `readStrOrNull(data, '${s.wireName}')`
2915
3003
  : `readStr(data, '${aggregate}', '${s.wireName}')`;
2916
3004
  return opt
2917
- ? `() { final v = ${reader}; return v == null ? null : ${s.valueClass}.fromRow(v); }()`
2918
- : `${s.valueClass}.fromRow(${reader})`;
3005
+ ? `() { final v = ${reader}; return v == null ? null : NmsInternalNomosRefs.create<${s.valueClass}>(workspace: _location, type: '${s.valueAggregateType}', id: v); }()`
3006
+ : `NmsInternalNomosRefs.create<${s.valueClass}>(workspace: _location, type: '${s.valueAggregateType}', id: ${reader})`;
2919
3007
  }
2920
3008
  return opt
2921
3009
  ? `readStrOrNull(data, '${s.wireName}')`
@@ -2970,8 +3058,8 @@ function parseReadModelField(s: AggFieldSpec, aggregate: string): string {
2970
3058
  }
2971
3059
 
2972
3060
  /** Emit the generated typed read-model class for one aggregate. The class
2973
- * (`SiteReadModel`, …) carries the kernel aggregate [aggregateId] (threaded from the
2974
- * `{type,id,data}` row id like the aggregate class) plus a DSL-named typed field per DSL
3061
+ * (`SiteReadModel`, …) carries the canonical aggregate-typed [ref] (composed from the
3062
+ * `{type,id,data}` row location) plus a DSL-named typed field per DSL
2975
3063
  * field declaration, and a [fromProjected] factory that reads the read engine's
2976
3064
  * already-decoded projected `data` shape. */
2977
3065
  function emitReadModel(
@@ -2979,17 +3067,18 @@ function emitReadModel(
2979
3067
  enumDartTypes: Map<string, string>,
2980
3068
  deriveds: DerivedDecl[] = [],
2981
3069
  combineds: CombinedDecl[] = [],
2982
- refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
3070
+ refIdClassForAggregate: (aggregateType: string) => string = concreteClassName,
2983
3071
  ownIdFields: ReadonlySet<string> = new Set(),
3072
+ aggregateWireTypeForAggregate: (aggregateType: string) => string = (aggregateType) => aggregateType,
2984
3073
  ): string {
2985
3074
  const className = readModelClassName(agg.id);
2986
- const idType = aggregateIdDartType(agg.id);
2987
3075
  const wireType = agg.id;
2988
3076
  const specs = aggregateFieldSpecs(
2989
3077
  agg,
2990
3078
  enumDartTypes,
2991
3079
  refIdClassForAggregate,
2992
3080
  ownIdFields,
3081
+ aggregateWireTypeForAggregate,
2993
3082
  );
2994
3083
 
2995
3084
  // DERIVED/COMBINED read fields are engine-projected JSON leaves. Their value schema is
@@ -3002,13 +3091,9 @@ function emitReadModel(
3002
3091
  // optional json-object leaf) is nullable; a PERMISSIVE json leaf is already the
3003
3092
  // nullable `Object?` (no extra `?`); a collection keeps its non-null container.
3004
3093
  const fieldDecls = [
3005
- ` /// The kernel aggregate id (the \`{type,id,data}\` row's \`id\`, e.g. the siteId).`,
3006
- ` /// Empty string only when built from a bare \`data\` map with no row id (a unit test).`,
3007
- ` final ${idType} aggregateId;`,
3008
- ` /// THE ONE REFERENCE (architecture/nomos_ref.md): this row's canonical, workspace-qualified [NomosRef]`,
3009
- ` /// (\`nomos://<workspace>/<type>/<id>\`) — composed at decode from the session workspace + the row's`,
3010
- ` /// type/id. The generated client accessors thread the session workspace; a bare decode leaves it ''.`,
3011
- ` final NomosRef ref;`,
3094
+ ` /// The canonical workspace-qualified identity of this ${concreteClassName(agg.id)}.`,
3095
+ ` /// Pass this exact value to generated actions, find and watch methods.`,
3096
+ ` final NomosRef<${concreteClassName(agg.id)}> ref;`,
3012
3097
  ...specs.flatMap((s) => {
3013
3098
  const doc = s.nullReason !== undefined ? [` /// null semantic: ${s.nullReason}`] : [];
3014
3099
  // SURFACED MERGE CONFLICTS: pending observations surface typed; resolve = ordinary next write.
@@ -3032,8 +3117,7 @@ function emitReadModel(
3032
3117
  ),
3033
3118
  ].join("\n");
3034
3119
  const ctorParams = [
3035
- ` this.aggregateId = const ${idType}.fromRow(''),`,
3036
- ` this.ref = const NomosRef(workspace: '', type: '${agg.id}', id: ''),`,
3120
+ ` required this.ref,`,
3037
3121
  ...specs.map((s) =>
3038
3122
  // A required field is `required`; an optional scalar, a collection, OR a
3039
3123
  // permissive json leaf (already nullable `Object?`) takes its default/null.
@@ -3074,8 +3158,7 @@ function emitReadModel(
3074
3158
  ];
3075
3159
 
3076
3160
  const parseLines = [
3077
- ` aggregateId: ${aggregateIdFromRowExpr(agg.id, "id")},`,
3078
- ` ref: NomosRef(workspace: workspace, type: type.isEmpty ? '${agg.id}' : type, id: id),`,
3161
+ ` ref: NmsInternalNomosRefs.create<${concreteClassName(agg.id)}>(workspace: _location, type: _kind.isEmpty ? '${aggregateWireTypeForAggregate(agg.id)}' : _kind, id: id),`,
3079
3162
  ...specs.map((s) =>
3080
3163
  s.surfaced === true
3081
3164
  ? ` ${s.name}: SurfacedValue.decode<${readModelDartType(s)}>(data, '${s.wireName}', ${slotDecodeCallback(s, "readModel")}),`
@@ -3111,10 +3194,7 @@ function emitReadModel(
3111
3194
  ` /// already decoded to bare/structured JSON), carrying the kernel aggregate [id] from`,
3112
3195
  ` /// the row \`id\` (defaults to '' for a bare-map call). PARTIAL folds: every scalar`,
3113
3196
  ` /// decodes \`null\` when absent (never throws); absent collections decode empty.`,
3114
- ` ///`,
3115
- ` /// [workspace] + [type] compose the canonical [ref] (both optional — the client accessors thread the`,
3116
- ` /// session workspace + the row type; a bare decode leaves workspace '' and falls back to the type '${agg.id}').`,
3117
- ` factory ${className}.fromProjected(Map<String, dynamic> data, [String id = '', String workspace = '', String type = '']) {`,
3197
+ ` factory ${className}.fromProjected(Map<String, dynamic> data, [String id = '', String _location = '', String _kind = '']) {`,
3118
3198
  ` return ${className}(`,
3119
3199
  parseLines,
3120
3200
  ` );`,
@@ -3124,39 +3204,63 @@ function emitReadModel(
3124
3204
  ].join("\n");
3125
3205
  }
3126
3206
 
3127
- // ---- public intent payload class -------------------------------------------
3207
+ // ---- typed action input class ----------------------------------------------
3128
3208
 
3129
3209
  function emitDirective(
3130
3210
  d: AnyDirective,
3131
3211
  domainName: string,
3132
3212
  enumDartTypes: Map<string, string>,
3133
3213
  lazyNames?: Map<z.ZodTypeAny, string>,
3214
+ aggregates: readonly AggregateHandle[] = [],
3134
3215
  ): string {
3135
- const className = `${cap(d.id)}Payload`;
3216
+ const className = dartActionInputClassName(d);
3136
3217
  const specs = introspectPayload(d.payloadSchema as z.ZodTypeAny, enumDartTypes, lazyNames);
3218
+ const recordInputs = dartBusinessRecordInputs(d, aggregates);
3219
+ const principalField = d.businessPrincipalField;
3220
+ const derivedFields = new Set([
3221
+ ...recordInputs.flatMap((input) => [input.idField, input.workspaceField]),
3222
+ ...(principalField === undefined ? [] : [principalField]),
3223
+ ]);
3224
+ const publicSpecs = specs.filter((spec) => !derivedFields.has(spec.name));
3225
+ const internalPayloadArguments = [
3226
+ ...(principalField === undefined ? [] : ["required String nmsInternalPrincipal"]),
3227
+ ...(recordInputs.length === 0
3228
+ ? []
3229
+ : [
3230
+ "required String Function(Object record) nmsInternalLocationOf",
3231
+ "required String Function(Object record) nmsInternalIdOf",
3232
+ ]),
3233
+ ];
3234
+ const internalPayloadSignature = internalPayloadArguments.length === 0
3235
+ ? ""
3236
+ : `{${internalPayloadArguments.join(", ")}}`;
3137
3237
 
3138
3238
  // Field decls: optional payload fields are nullable Dart fields (omitted from the
3139
3239
  // payload JSON when null); required fields are non-null + `required` in the ctor.
3140
- const fieldDecls = specs
3240
+ const fieldDecls = [
3241
+ ...publicSpecs
3141
3242
  .map((s) =>
3142
3243
  s.nullAdmits === true
3143
3244
  ? ` final NomosSlot<${stripNullable(s.dartType)}> ${s.name};`
3144
3245
  : ` final ${s.dartType}${s.optional ? "?" : ""} ${s.name};`,
3145
- )
3146
- .join("\n");
3147
- const ctorParams = specs
3246
+ ),
3247
+ ...recordInputs.map((input) => ` final ${input.dartType} ${input.argument};`),
3248
+ ].join("\n");
3249
+ const ctorParams = [
3250
+ ...publicSpecs
3148
3251
  .map((s) =>
3149
3252
  // A null-admitting slot defaults to Omitted (the call site chooses Present/ExplicitNull);
3150
3253
  // an optional field defaults to null; a required field is `required`.
3151
3254
  s.nullAdmits === true
3152
3255
  ? ` this.${s.name} = const NomosSlot.omitted(),`
3153
3256
  : ` ${s.optional ? "" : "required "}this.${s.name},`,
3154
- )
3155
- .join("\n");
3257
+ ),
3258
+ ...recordInputs.map((input) => ` required this.${input.argument},`),
3259
+ ].join("\n");
3156
3260
  // Zero-field payload (e.g. a `.births()` sanctioning directive with an empty
3157
3261
  // object schema): `const X({});` / a dangling blank field block is INVALID Dart —
3158
3262
  // emit a plain `const X();` and an empty map literal instead.
3159
- const hasFields = specs.length > 0;
3263
+ const hasFields = publicSpecs.length > 0 || recordInputs.length > 0;
3160
3264
  const ctorLines = hasFields
3161
3265
  ? [` const ${className}({`, ctorParams, ` });`]
3162
3266
  : [` const ${className}();`];
@@ -3166,17 +3270,17 @@ function emitDirective(
3166
3270
  // fields are written only when present (mirrors the engine's Zod: ops are emitted
3167
3271
  // only for present fields).
3168
3272
  const requiredEntries = specs
3169
- .filter((s) => !s.optional && s.nullAdmits !== true)
3273
+ .filter((s) => !derivedFields.has(s.name) && !s.optional && s.nullAdmits !== true)
3170
3274
  .map((s) => ` '${s.name}': ${toJsonExpr(s, s.name)},`);
3171
3275
  const optionalLines = specs
3172
- .filter((s) => s.optional && s.nullAdmits !== true)
3276
+ .filter((s) => !derivedFields.has(s.name) && s.optional && s.nullAdmits !== true)
3173
3277
  .map(
3174
3278
  (s) =>
3175
3279
  ` if (${s.name} != null) '${s.name}': ${toJsonExpr(s, `${s.name}!`)},`,
3176
3280
  );
3177
3281
  // NULL SEMANTICS: a null-admitting field writes through its slot — Omitted writes nothing,
3178
3282
  // ExplicitNull writes a real `null`, Present writes the encoded value (null_semantics.md).
3179
- const slotSpecs = specs.filter((s) => s.nullAdmits === true);
3283
+ const slotSpecs = specs.filter((s) => !derivedFields.has(s.name) && s.nullAdmits === true);
3180
3284
  const slotWriteLines = slotSpecs.map(
3181
3285
  (s) => ` ${s.name}.writeTo(data, '${s.name}', (v) => ${toJsonExpr(s, "v")});`,
3182
3286
  );
@@ -3186,51 +3290,43 @@ function emitDirective(
3186
3290
 
3187
3291
  return [
3188
3292
  `/// ${docFirst}`,
3293
+ `/// Business facts accepted by this generated application action.`,
3189
3294
  `///`,
3190
- `/// Generated typed intent DTO for '${d.id}' (marker: ${d.marker},`,
3191
- `/// aggregate: ${d.aggregateId}). It carries its [domain] + [intentType] and a`,
3192
- `/// [toPayloadJson] that ships only typed fields as data. It builds no`,
3193
- `/// \`Event\` and no \`Hlc\`. Pass it to [offer]; the GitHolon executes`,
3194
- `/// the installed domain plan.`,
3195
- `class ${className} implements NomosIntent {`,
3196
- ` /// The policy domain this intent belongs to (the kernel \`author\` \`domain\`).`,
3197
- ` static const String domainName = '${domainName}';`,
3198
- ` /// The public intent type within [domainName] — never hand-typed at the call site.`,
3199
- ` static const String intentTypeName = '${d.id}';`,
3200
- ` /// The aggregate this intent targets (provenance only; the engine binds the instance).`,
3201
- ` static const String aggregateId = '${d.aggregateId}';`,
3202
- ` /// The referential marker (creates/mutates/ensures/archives).`,
3203
- ` static const String marker = '${d.marker}';`,
3295
+ `/// Pass this value straight to the generated domain application. It is a command,`,
3296
+ `/// not application state; do not retain it in a repository or submit it through runtime APIs.`,
3297
+ `class ${className} {`,
3204
3298
  ``,
3205
3299
  ...(hasFields ? [fieldDecls, ``] : []),
3206
3300
  ` /// ${docFirst}`,
3207
3301
  ...ctorLines,
3208
3302
  ``,
3209
- ` @override`,
3210
- ` String get domain => domainName;`,
3211
- ``,
3212
- ` @override`,
3213
- ` String get intentType => intentTypeName;`,
3214
- ``,
3215
- ` /// Serialise this payload to the intent DATA JSON (the shape the`,
3216
- ` /// domain Zod schema validates inside the engine). Optional fields are`,
3217
- ` /// omitted when null; a null-admitting field writes through its tri-state slot.`,
3218
- ` @override`,
3303
+ ` /// Nomos implementation seam used only by the generated application package.`,
3304
+ ` /// Tenant code should pass this object to the generated domain application instead.`,
3219
3305
  ...(hasSlots
3220
3306
  ? [
3221
- ` Map<String, Object?> toPayloadJson() {`,
3307
+ ` Map<String, Object?> nmsInternalToPayloadJson(${internalPayloadSignature}) {`,
3222
3308
  ` final data = <String, Object?>{`,
3223
3309
  ...requiredEntries,
3224
3310
  ...optionalLines,
3311
+ ...(principalField === undefined ? [] : [` '${principalField}': nmsInternalPrincipal,`]),
3312
+ ...recordInputs.flatMap((input) => [
3313
+ ` '${input.workspaceField}': nmsInternalLocationOf(${input.argument}),`,
3314
+ ` '${input.idField}': nmsInternalIdOf(${input.argument}),`,
3315
+ ]),
3225
3316
  ` };`,
3226
3317
  ...slotWriteLines,
3227
3318
  ` return data;`,
3228
3319
  ` }`,
3229
3320
  ]
3230
3321
  : [
3231
- ` Map<String, Object?> toPayloadJson() => <String, Object?>{`,
3322
+ ` Map<String, Object?> nmsInternalToPayloadJson(${internalPayloadSignature}) => <String, Object?>{`,
3232
3323
  ...requiredEntries,
3233
3324
  ...optionalLines,
3325
+ ...(principalField === undefined ? [] : [` '${principalField}': nmsInternalPrincipal,`]),
3326
+ ...recordInputs.flatMap((input) => [
3327
+ ` '${input.workspaceField}': nmsInternalLocationOf(${input.argument}),`,
3328
+ ` '${input.idField}': nmsInternalIdOf(${input.argument}),`,
3329
+ ]),
3234
3330
  ` };`,
3235
3331
  ]),
3236
3332
  `}`,
@@ -3252,6 +3348,10 @@ export function toJsonExpr(spec: PayloadFieldSpec, accessor: string): string {
3252
3348
  return `${accessor}.map((e) => e.toJson()).toList()`;
3253
3349
  case "wrapped":
3254
3350
  return `${accessor}.value`;
3351
+ case "ref":
3352
+ return `NmsInternalNomosRefs.id(${accessor})`;
3353
+ case "recipient":
3354
+ return `NmsInternalRecipients.address(${accessor})`;
3255
3355
  }
3256
3356
  }
3257
3357
 
@@ -3316,7 +3416,7 @@ export function synthesizeRecursivePayloadTypes(
3316
3416
  const b = brandedIdMetaOf(zt);
3317
3417
  if (b !== undefined) return { type: "string", brand: b };
3318
3418
  const idAgg = idOfMetaOf(zt);
3319
- if (idAgg !== undefined) return { type: "string", brand: `${idAgg}Id` };
3419
+ if (idAgg !== undefined) return { type: "string", idOf: idAgg };
3320
3420
  return { type: "string" };
3321
3421
  }
3322
3422
  case "ZodNumber": {
@@ -3450,7 +3550,7 @@ function emitQueryReads(
3450
3550
  q: QueryDecl,
3451
3551
  aggregatesById: Map<string, AggregateHandle>,
3452
3552
  enumDartTypes: Map<string, string>,
3453
- refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
3553
+ refIdClassForAggregate: (aggregateType: string) => string = concreteClassName,
3454
3554
  ownIdFieldsByAggregate: ReadonlyMap<string, ReadonlySet<string>> = new Map(),
3455
3555
  ): string {
3456
3556
  const agg = q.returns; // the aggregate TYPE id (== the wire id, used for field/index lookups).
@@ -3507,12 +3607,12 @@ function emitAggregateByIdReads(agg: AggregateHandle): string {
3507
3607
  `/// (\`null\` until/unless it has been folded). Emits the current value, then`,
3508
3608
  `/// re-emits on each projection tick. Primary-key lookup (#140) — O(1).`,
3509
3609
  `Stream<${cls}?> watch${cls}ById(NomosReads reads, ${idType} id) =>`,
3510
- ` reads.watchById(id.value, ${cls}.fromJson);`,
3610
+ ` reads.watchById(NmsInternalNomosRefs.id(id), ${cls}.fromJson);`,
3511
3611
  ``,
3512
3612
  `/// One-shot typed by-id read: the CURRENT \`${cls}\` with kernel aggregate id`,
3513
3613
  `/// [id], or \`null\` when no such aggregate has been folded. O(1).`,
3514
3614
  `${cls}? read${cls}ById(NomosReads reads, ${idType} id) =>`,
3515
- ` reads.readById(id.value, ${cls}.fromJson);`,
3615
+ ` reads.readById(NmsInternalNomosRefs.id(id), ${cls}.fromJson);`,
3516
3616
  ].join("\n");
3517
3617
  }
3518
3618
 
@@ -3541,7 +3641,7 @@ function emitCountReads(
3541
3641
  c: ReturnType<typeof finishCount>,
3542
3642
  aggregatesById: Map<string, AggregateHandle>,
3543
3643
  enumDartTypes: Map<string, string>,
3544
- refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
3644
+ refIdClassForAggregate: (aggregateType: string) => string = concreteClassName,
3545
3645
  ownIdFieldsByAggregate: ReadonlyMap<string, ReadonlySet<string>> = new Map(),
3546
3646
  ): string {
3547
3647
  const method = readMethodName(c.id);
@@ -3593,7 +3693,7 @@ function emitSumReads(
3593
3693
  s: ReturnType<typeof finishSum>,
3594
3694
  aggregatesById: Map<string, AggregateHandle>,
3595
3695
  enumDartTypes: Map<string, string>,
3596
- refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
3696
+ refIdClassForAggregate: (aggregateType: string) => string = concreteClassName,
3597
3697
  ownIdFieldsByAggregate: ReadonlyMap<string, ReadonlySet<string>> = new Map(),
3598
3698
  ): string {
3599
3699
  const method = readMethodName(s.id);
@@ -3647,7 +3747,7 @@ function emitMinReads(
3647
3747
  e: ReturnType<typeof finishExtremum>,
3648
3748
  aggregatesById: Map<string, AggregateHandle>,
3649
3749
  enumDartTypes: Map<string, string>,
3650
- refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
3750
+ refIdClassForAggregate: (aggregateType: string) => string = concreteClassName,
3651
3751
  ownIdFieldsByAggregate: ReadonlyMap<string, ReadonlySet<string>> = new Map(),
3652
3752
  ): string {
3653
3753
  const method = readMethodName(e.id);
@@ -3700,7 +3800,7 @@ function emitMaxReads(
3700
3800
  e: ReturnType<typeof finishExtremum>,
3701
3801
  aggregatesById: Map<string, AggregateHandle>,
3702
3802
  enumDartTypes: Map<string, string>,
3703
- refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
3803
+ refIdClassForAggregate: (aggregateType: string) => string = concreteClassName,
3704
3804
  ownIdFieldsByAggregate: ReadonlyMap<string, ReadonlySet<string>> = new Map(),
3705
3805
  ): string {
3706
3806
  const method = readMethodName(e.id);
@@ -3755,7 +3855,7 @@ function emitExistsReads(
3755
3855
  e: ReturnType<typeof finishExists>,
3756
3856
  aggregatesById: Map<string, AggregateHandle>,
3757
3857
  enumDartTypes: Map<string, string>,
3758
- refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
3858
+ refIdClassForAggregate: (aggregateType: string) => string = concreteClassName,
3759
3859
  ownIdFieldsByAggregate: ReadonlyMap<string, ReadonlySet<string>> = new Map(),
3760
3860
  ): string {
3761
3861
  const method = readMethodName(e.id);
@@ -3837,23 +3937,10 @@ function emitOrderedReads(d: OrderedReadDecl): string {
3837
3937
  ].join("\n");
3838
3938
  }
3839
3939
 
3840
- // ---- minted typed-id creates (the kernel id-mint keystone) -----------------
3841
- //
3842
- // A `.creates` directive whose payload carries a target-id field (the id bound to the
3843
- // NEW aggregate) is a KERNEL-MINTED create: the frontend must NOT pass a raw id. For
3844
- // such a directive we emit, ALONGSIDE the plain payload DTO:
3845
- // * a typed id value class (e.g. `SiteId`) wrapping the kernel-minted string, so a
3846
- // raw `String` is never accepted where a minted id is required, and
3847
- // * a top-level `create…({Minter mint, …})` factory that calls the KERNEL mint (no
3848
- // raw id param, NO seed) to RESERVE the typed id and builds the payload from it — so
3849
- // the only way to construct a minted create at the call site is THROUGH the kernel
3850
- // mint, and the dev never sees a uuid/seed/nonce.
3851
- //
3852
- // THE MODEL: the KERNEL mints `<Type>_<uuidv7>` from the HOST-INJECTED rng; the id is
3853
- // CAPTURED in the create event and READ on replay (never re-minted). The UUID body is
3854
- // opaque uniqueness only — NOT time/order; intent HLCs carry ordering. There is NO
3855
- // `mintSeed` payload field any more — the id itself is the proof. The factory passes
3856
- // NOTHING to the mint but the aggregate type.
3940
+ // ---- legacy payload-bound own-id discovery ---------------------------------
3941
+ // Some old laws copied a payload string into their created aggregate's own id field. We still trace
3942
+ // that shape so decoded legacy fields become `NomosRef<TAggregate>`. No FooId class or frontend mint
3943
+ // factory is emitted; current creates are law-minted and return their canonical reference.
3857
3944
 
3858
3945
  /**
3859
3946
  * The target-id field of a minted create: the payload field bound to the aggregate the
@@ -3868,11 +3955,11 @@ function emitOrderedReads(d: OrderedReadDecl): string {
3868
3955
  * Mechanism: probe each top-level STRING field with a unique sentinel, run `.plan()`, find
3869
3956
  * the `Create` event whose `__type` == the directive's `.creates` aggregate, and map its
3870
3957
  * `__id` sentinel back to the payload field. Returns `undefined` when the created aggregate's
3871
- * id is not a verbatim top-level payload string (then no minting factory is emitted).
3958
+ * id is not a verbatim top-level payload string.
3872
3959
  *
3873
3960
  * `agg` is the `.creates` target handle (`directive.aggregateId`), required by `executeDirectiveToIntent`.
3874
- * If the plan cannot be traced, emit no factory. Guessing by field name makes the
3875
- * public Dart surface lie about aggregate identity.
3961
+ * If the plan cannot be traced, do not classify an own-id field. Guessing by field name would make
3962
+ * the decoded Dart surface lie about aggregate identity.
3876
3963
  */
3877
3964
  function mintedIdField(
3878
3965
  d: AnyDirective,
@@ -3987,161 +4074,6 @@ function sampleFromZod(zt: z.ZodTypeAny): unknown {
3987
4074
  }
3988
4075
  }
3989
4076
 
3990
- /** The Dart typed-id class name for a minted create's id field. Unified on the
3991
- * aggregate WIRE TYPE (so `createSite`'s `siteId` → `SiteId`, the same name a ref
3992
- * targeting `SiteRootAggregate` uses), NOT on the payload field name.
3993
- * The minting factory uses `FooId._(mintedId)` (the truly private ctor, same file). */
3994
- function typedIdClassName(aggregateType: string): string {
3995
- return typedIdClassForAggregateType(aggregateType);
3996
- }
3997
-
3998
- /** Emit a typed id value class for an aggregate TYPE. Type-safe: a raw `String` — or
3999
- * another aggregate's typed id — is a COMPILE ERROR where a [${cls}] is required (the
4000
- * value classes are distinct nominal types). Public constructors:
4001
- * * [${cls}] / [${cls}.fromString] — non-strict wrappers for app route params,
4002
- * existing refs, and test fixtures.
4003
- * * [${cls}.fromMinted] — STRICT: asserts the kernel type tag matches.
4004
- * One row-id constructor:
4005
- * * [${cls}.fromRow] — wraps a kernel-stored row/ref id when deserializing a
4006
- * projection row. This intentionally does NOT enforce the minted type tag because
4007
- * reads may include not-yet-minted ids; write admission remains the mint gate.
4008
- */
4009
- function emitTypedIdClass(aggregateType: string): string {
4010
- const cls = typedIdClassName(aggregateType);
4011
- return [
4012
- `/// Typed id for \`${aggregateType}\`. A DISTINCT nominal type so a raw String — or`,
4013
- `/// ANOTHER aggregate's typed id — is a COMPILE ERROR where a [${cls}] is required.`,
4014
- `/// [fromMinted] is the strict constructor for ids reserved by the kernel mint.`,
4015
- `/// [${cls}] / [fromString] / [fromRow] are non-strict wrappers for route params,`,
4016
- `/// existing refs, and generated projection decoding.`,
4017
- `class ${cls} {`,
4018
- ` /// The kernel-minted id string (\`${aggregateType}_<uuidv7>\`).`,
4019
- ` /// The UUID body is opaque uniqueness only — NOT time/order; intent HLCs carry ordering.`,
4020
- ` final String value;`,
4021
- ` const ${cls}(this.value);`,
4022
- ` const ${cls}._(this.value);`,
4023
- ` factory ${cls}.fromString(String value) => ${cls}.fromRow(value);`,
4024
- ` /// Wrap a kernel-stored row/ref id while decoding a projection row. This is`,
4025
- ` /// intentionally non-strict: old ledgers and cross-domain refs may not carry the`,
4026
- ` /// minted type tag, so admission/minting is enforced on writes, not reads.`,
4027
- ` const ${cls}.fromRow(this.value);`,
4028
- ` /// Wrap an ALREADY-kernel-minted id string (e.g. one read back from a query).`,
4029
- ` /// STRICT: asserts the type tag matches \`${aggregateType}\` — a wrong-typed id throws.`,
4030
- ` /// A minted id is \`<TypeTag>_<uuidv7>\`, so the tag is the prefix before the FIRST \`_\`.`,
4031
- ` /// Do not read time or ordering from the UUID body; it is an opaque uniqueness token.`,
4032
- ` factory ${cls}.fromMinted(String minted) {`,
4033
- ` final i = minted.indexOf('_');`,
4034
- ` final tag = i < 0 ? minted : minted.substring(0, i);`,
4035
- ` if (tag != '${aggregateType}') {`,
4036
- ` throw ArgumentError('id "$minted" is not a ${aggregateType} id (tag "$tag")');`,
4037
- ` }`,
4038
- ` return ${cls}._(minted);`,
4039
- ` }`,
4040
- ` @override`,
4041
- ` bool operator ==(Object other) => other is ${cls} && other.value == value;`,
4042
- ` @override`,
4043
- ` int get hashCode => value.hashCode;`,
4044
- ` @override`,
4045
- ` String toString() => '${cls}($value)';`,
4046
- `}`,
4047
- ].join("\n");
4048
- }
4049
-
4050
- /** Emit the top-level minted `create…(...)` factory function: it calls the KERNEL mint
4051
- * (NO raw id param, NO seed) to RESERVE a typed id and builds the payload from it,
4052
- * returning `(payload, typedId)`. This is the ONLY way to construct a minted create at the
4053
- * call site — the dev supplies NOTHING about the id. The factory itself contains NO
4054
- * id/uuid/seed construction — only the runtime kernel-mint call via [mint]. */
4055
- function emitMintedFactory(
4056
- d: AnyDirective,
4057
- _domainName: string,
4058
- idField: PayloadFieldSpec,
4059
- specs: PayloadFieldSpec[],
4060
- aggregateType: string,
4061
- ): string {
4062
- const className = `${cap(d.id)}Payload`;
4063
- const idCls = typedIdClassName(aggregateType);
4064
- const fnName = d.id; // e.g. `createSite` — the typed minting factory.
4065
- // The caller supplies every field EXCEPT the minted id (the kernel mint produces it).
4066
- // Optional fields become optional named params. There is NO seed/author param: the
4067
- // kernel mints the UUIDv7 from the host-injected rng — the dev touches none of it.
4068
- const callerFields = specs.filter((s) => s.name !== idField.name);
4069
- const ctorParams = callerFields
4070
- .map((s) => ` ${s.optional ? "" : "required "}${s.dartType}${s.optional ? "?" : ""} ${s.name},`)
4071
- .join("\n");
4072
- const forwarded = callerFields.map((s) => ` ${s.name}: ${s.name},`).join("\n");
4073
- return [
4074
- `/// KERNEL-MINTED \`${d.id}\` factory: calls [mint] (the kernel id-mint) to RESERVE a`,
4075
- `/// fresh typed [${idCls}] — NO raw id param, NO seed — and ships the minted id in the`,
4076
- `/// payload, so the kernel GATE verifies its shape + type tag on author. The kernel`,
4077
- `/// mints the \`${aggregateType}_<uuidv7>\` from the HOST-INJECTED rng (native OsRng / web`,
4078
- `/// crypto). The UUID body is opaque uniqueness only — NOT time/order; intent HLCs carry`,
4079
- `/// ordering. This factory builds NO id/uuid/seed itself — only the runtime mint call.`,
4080
- `/// Returns the [${className}] AND the minted [${idCls}] (so the caller can reference the`,
4081
- `/// new aggregate). This is the type-safe create path: the frontend cannot supply a raw id.`,
4082
- `(${className}, ${idCls}) ${fnName}({`,
4083
- ` required Minter mint,`,
4084
- ctorParams,
4085
- `}) {`,
4086
- ` final mintedId = mint(aggregateType: '${aggregateType}');`,
4087
- ` final payload = ${className}(`,
4088
- ` ${idField.name}: mintedId,`,
4089
- forwarded,
4090
- ` );`,
4091
- ` return (payload, ${idCls}._(mintedId));`,
4092
- `}`,
4093
- ].join("\n");
4094
- }
4095
-
4096
- /** Emit the typed-id keystone for a domain (GENERALIZED across ALL aggregates):
4097
- * 1. a DISTINCT typed id value class PER AGGREGATE (so cross-aggregate type
4098
- * confusion is a compile error + ref fields have a target type to name); and
4099
- * 2. a kernel-minting `create…` factory for every `.creates` directive whose real
4100
- * plan trace proves a payload field is the created aggregate id.
4101
- * Every aggregate gets a typed id even if it has no minting factory (a singleton /
4102
- * kernel-minted create), because OTHER aggregates' ref fields may target it.
4103
- * Returns `""` when the domain has no aggregates (nothing to emit). */
4104
- function emitMintedCreates(
4105
- aggregates: AggregateHandle[],
4106
- directives: AnyDirective[],
4107
- domainName: string,
4108
- enumDartTypes: Map<string, string>,
4109
- /** Aggregate ids whose typed-id CLASS is emitted in the shared `nomos_framework.dart` (imported
4110
- * here), so it must NOT be re-declared. Minting FACTORIES still emit and reference the imported
4111
- * class. Empty = emit every id class locally (the single-domain / shared-file path). */
4112
- skipIdClassFor: ReadonlySet<string> = new Set(),
4113
- ): string {
4114
- const blocks: string[] = [];
4115
-
4116
- // (1) A typed id class per aggregate. Dedup by the typed-id class NAME: two wire
4117
- // types never collide on a name (the name is a pure function of the type), and a
4118
- // domain that re-exports a framework aggregate must not emit its id class twice.
4119
- // Framework-shared aggregates are emitted ONCE in the package's `nomos_framework.dart`,
4120
- // so their id class is SKIPPED here (referenced via import).
4121
- const emittedIdClasses = new Set<string>();
4122
- for (const agg of aggregates) {
4123
- if (skipIdClassFor.has(agg.id)) continue;
4124
- const cls = typedIdClassForAggregateType(agg.id);
4125
- if (emittedIdClasses.has(cls)) continue;
4126
- emittedIdClasses.add(cls);
4127
- blocks.push(emitTypedIdClass(agg.id));
4128
- }
4129
-
4130
- // (2) A minting factory per qualifying create — the kernel mints the `.creates` target's
4131
- // id, identified MARKER-DRIVEN by running the real `.plan()` (see `mintedIdField`).
4132
- const aggById = new Map<string, AggregateHandle>();
4133
- for (const a of aggregates) aggById.set(a.id, a);
4134
- for (const d of directives) {
4135
- if (d.marker !== "creates") continue;
4136
- const specs = introspectPayload(d.payloadSchema as z.ZodTypeAny, enumDartTypes);
4137
- const idField = mintedIdField(d, aggById.get(d.aggregateId), specs);
4138
- if (idField === undefined) continue;
4139
- const aggregateType = d.aggregateId;
4140
- blocks.push(emitMintedFactory(d, domainName, idField, specs, aggregateType));
4141
- }
4142
- return blocks.join("\n\n");
4143
- }
4144
-
4145
4077
  function collectOwnIdFieldsByAggregate(
4146
4078
  aggregates: AggregateHandle[],
4147
4079
  directives: AnyDirective[],
@@ -4180,6 +4112,18 @@ export interface DomainModule {
4180
4112
  domain?: string;
4181
4113
  aggregates: AggregateHandle[];
4182
4114
  directives: AnyDirective[];
4115
+ /**
4116
+ * The domain's complete public business language. When present, generated
4117
+ * application clients publish only these intent methods; their private
4118
+ * constituent directives have already been inlined into the executable plans.
4119
+ * Application metadata only: executable law remains in {@link directives}.
4120
+ */
4121
+ applicationIntents?: AnyDirective[];
4122
+ /**
4123
+ * Durable cross-workspace business sharing policies. Code-generation metadata
4124
+ * only: the order and receipt directives remain ordinary certified law.
4125
+ */
4126
+ applicationWorkspaceSharing?: import("./workspace_sharing.js").WorkspaceSharingPolicy[];
4183
4127
  /**
4184
4128
  * SECONDARY-CREATE AUTO-DECLARE (secondary_ensure.ts) — the aggregate TYPES whose deterministic
4185
4129
  * secondary `ensure(...)` `composeDomainModule` plan-probed and SANCTIONED by injecting a no-op
@@ -4346,7 +4290,7 @@ export interface DomainModule {
4346
4290
  valueObjects?: readonly ValueObjectDescriptor[];
4347
4291
  /**
4348
4292
  * DOMAIN CONTRACTS (`architecture/domain_contracts.md`) — the public DTO shapes this domain declares
4349
- * (`contract(id, fields)`), discovered by shape and composed across the domain's modules. ADDITIVE +
4293
+ * (`recordContract(id, fields)`), discovered by shape and composed across the domain's modules. ADDITIVE +
4350
4294
  * OPTIONAL: a contract-free domain omits this entirely and is byte-identical to before contracts existed.
4351
4295
  * Contracts are PURE typing/codegen metadata (NOT hashed law); they drive the additive `nomosContracts`
4352
4296
  * IR key + the shared `contracts.dart` DTO classes + the TS `<Id>Contract` interfaces. `implementedBy`
@@ -4425,12 +4369,12 @@ export interface DomainModule {
4425
4369
  permissionVocabulary?: PermissionVocabulary;
4426
4370
  /**
4427
4371
  * Extra Dart imports needed by this generated file. Used when a domain has typed
4428
- * refs to aggregate id classes emitted by another generated domain file.
4372
+ * refs to aggregate classes emitted by another generated domain file.
4429
4373
  */
4430
4374
  dartImports?: DartImport[];
4431
4375
  /**
4432
4376
  * Mapping from referenced aggregate wire type to the Dart import prefix that exposes
4433
- * that aggregate's typed id class. This preserves cross-domain ref typing without
4377
+ * that aggregate's generated class. This preserves cross-domain ref typing without
4434
4378
  * forcing every domain into one giant generated library.
4435
4379
  */
4436
4380
  dartRefImports?: DartRefImport[];
@@ -4438,15 +4382,19 @@ export interface DomainModule {
4438
4382
 
4439
4383
  /** Options controlling FRAMEWORK-SHARED aggregate deduplication (Dart-codegen hygiene). */
4440
4384
  export interface GenerateDartDomainOptions {
4385
+ /** Package-wide aggregate ownership used to emit canonical domain-qualified reference types. */
4386
+ readonly packageAggregateDomains?: Readonly<
4387
+ Record<string, { domain: string; name: string; namespaced?: boolean }>
4388
+ >;
4441
4389
  /**
4442
- * Aggregate ids whose Dart surface (projection class, read model, typed-id class, by-id accessors)
4390
+ * Aggregate ids whose Dart surface (projection class, read model, by-id accessors)
4443
4391
  * is emitted ONCE per package in the shared `nomos_framework.dart` and MUST NOT be re-emitted here —
4444
4392
  * this domain REFERENCES them via {@link frameworkImportUri}. Empty/absent ⇒ emit everything locally
4445
4393
  * (the legacy single-file behaviour; byte-identical to before this option existed).
4446
4394
  */
4447
4395
  readonly skipAggregateIds?: ReadonlySet<string>;
4448
4396
  /**
4449
- * Framework-shared DIRECTIVE ids whose generated payload class (+ minted-create factory) is emitted
4397
+ * Framework-shared DIRECTIVE ids whose generated payload class is emitted
4450
4398
  * ONCE per package in the shared file and MUST NOT be re-emitted here. The domain still OFFERS them
4451
4399
  * (the client offer methods stay per-domain); only the payload TYPE moves to the shared file.
4452
4400
  */
@@ -4480,7 +4428,7 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
4480
4428
  mod = {
4481
4429
  ...mod,
4482
4430
  aggregates: publicAggregates,
4483
- directives: mod.directives.filter(
4431
+ directives: (mod.applicationIntents ?? mod.directives).filter(
4484
4432
  (d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
4485
4433
  ),
4486
4434
  ...(mod.queries === undefined
@@ -4498,10 +4446,16 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
4498
4446
  const codegenDirectives = mod.directives
4499
4447
  .filter((d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true)
4500
4448
  .map((d) => {
4501
- const businessBirth = (d as { businessBirth?: { publicPayloadSchema?: z.ZodTypeAny } }).businessBirth;
4449
+ const businessBirth = (d as { businessBirth?: { aggregateId?: string; publicPayloadSchema?: z.ZodTypeAny } }).businessBirth;
4502
4450
  return businessBirth?.publicPayloadSchema === undefined
4503
4451
  ? d
4504
- : { ...d, payloadSchema: businessBirth.publicPayloadSchema } as AnyDirective;
4452
+ : {
4453
+ ...d,
4454
+ // Frontend-only name: the lowered framework directive remains `nomosBirth<Aggregate>` in law,
4455
+ // while tenant code receives the ordinary business input `Create<Aggregate>Input`.
4456
+ id: isBusinessIntent(d) ? d.id : `create${businessBirth.aggregateId ?? cap(d.id)}`,
4457
+ payloadSchema: businessBirth.publicPayloadSchema,
4458
+ } as AnyDirective;
4505
4459
  });
4506
4460
  const skipAggregateIds = opts.skipAggregateIds ?? new Set<string>();
4507
4461
  const skipDirectiveIds = opts.skipDirectiveIds ?? new Set<string>();
@@ -4530,13 +4484,12 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
4530
4484
  // in local VO descriptors OR directive payloads gets ONE zero-cost wrapper. Opaque by design —
4531
4485
  // no generated arithmetic/conversions (a physics library is not a type system).
4532
4486
  const unitNames = new Set<string>();
4533
- // brandedId(...) names → their own zero-cost String extension type (the standalone twin of
4534
- // the aggregate typed-id classes). Collected from VO descriptors AND directive payloads.
4487
+ // brandedId(...) names → their own zero-cost String extension type. These represent external
4488
+ // nominal strings, never aggregate identity. Collected from VO descriptors and directive payloads.
4535
4489
  const brandNames = new Set<string>();
4536
4490
  const walkUnitField = (f: ValueObjectFieldDescriptor): void => {
4537
4491
  if (f.type === "number" && f.unit !== undefined) unitNames.add(f.unit);
4538
- // A standalone brandedId declares its own extension type; an idOf REFERENCE does NOT — the aggregate
4539
- // emits the canonical `<Agg>Id` class (with `.fromRow`), so re-declaring it here dup'd the symbol (co2).
4492
+ // A standalone brandedId declares its own extension type; an idOf REFERENCE does not.
4540
4493
  else if (f.type === "string" && f.brand !== undefined && f.idOf === undefined) brandNames.add(f.brand);
4541
4494
  else if (f.type === "array") walkUnitField(f.items);
4542
4495
  else if (f.type === "map") walkUnitField(f.values);
@@ -4552,8 +4505,7 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
4552
4505
  if (schema && typeof schema === "object" && zodTypeName(schema as z.ZodTypeAny) === "ZodObject") {
4553
4506
  try {
4554
4507
  for (const spec of introspectPayload(schema as z.ZodTypeAny, new Map())) {
4555
- // A spec whose brand a VO descriptor already registered is a brandedId, not a unit —
4556
- // and neither is an aggregate typed-id (`<Agg>Id`, emitted by the id machinery).
4508
+ // A wrapped spec whose brand a VO descriptor already registered is a brandedId, not a unit.
4557
4509
  if (spec.toJson === "wrapped" && !spec.dartType.endsWith("Id") && !brandNames.has(spec.dartType)) {
4558
4510
  unitNames.add(spec.dartType);
4559
4511
  }
@@ -4621,8 +4573,19 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
4621
4573
  domainKey,
4622
4574
  );
4623
4575
  const refIdClassForAggregate = refIdClassResolver(mod);
4576
+ const packageAggregateDomains = opts.packageAggregateDomains ?? {};
4624
4577
  const aggregatesById = new Map<string, AggregateHandle>();
4625
4578
  for (const a of mod.aggregates) aggregatesById.set(a.id, a);
4579
+ const aggregateWireTypeForAggregate = (aggregateType: string): string => {
4580
+ const local = aggregatesById.get(aggregateType);
4581
+ if (local !== undefined && !isNamespacedAggregate(local)) return aggregateType;
4582
+ const owner = packageAggregateDomains[aggregateType];
4583
+ if (owner?.namespaced === false) return aggregateType;
4584
+ const domain = owner?.domain ?? (local === undefined ? undefined : domainKey);
4585
+ return domain === undefined
4586
+ ? aggregateType
4587
+ : qualifyReadProjectionType(domain, aggregateType);
4588
+ };
4626
4589
  const ownIdFieldsByAggregate = collectOwnIdFieldsByAggregate(
4627
4590
  mod.aggregates,
4628
4591
  codegenDirectives,
@@ -4637,24 +4600,16 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
4637
4600
  dartTypes,
4638
4601
  refIdClassForAggregate,
4639
4602
  ownIdFieldsByAggregate.get(a.id) ?? new Set(),
4603
+ aggregateWireTypeForAggregate,
4640
4604
  ),
4641
4605
  )
4642
4606
  .join("\n\n");
4643
4607
  // the runtime aggregate-meta registry (lookup by op.aggregateType — generic surfaces).
4644
4608
  const aggregateMetaBlock = emitAggregateMetaRegistry(domainKey, localAggregates);
4645
4609
  const dirBlock = localDirectives
4646
- .map((d) => emitDirective(d, domainKey, dartTypes, recursiveTypes.names))
4610
+ .map((d) => emitDirective(d, domainKey, dartTypes, recursiveTypes.names, mod.aggregates))
4647
4611
  .join("\n\n");
4648
4612
 
4649
- // TYPED IDS + MINTED CREATES (the id-mint keystone, GENERALIZED): a DISTINCT typed
4650
- // id class for EVERY aggregate (cross-aggregate type-confusion is a compile error;
4651
- // ref fields name their target's typed id), plus a top-level `create…` factory that
4652
- // mints THROUGH the kernel (no raw id) for every `.creates` directive carrying a
4653
- // `mintSeed` + an own-id field.
4654
- // Minted-create factories for framework directives (if any) live in the shared file — pass only the
4655
- // LOCAL directives here; framework typed-id classes are likewise skipped (referenced via import).
4656
- const mintedBlock = emitMintedCreates(mod.aggregates, localDirectives, domainKey, dartTypes, skipAggregateIds);
4657
-
4658
4613
  // PROVIDER SDK (the symmetric twin of the peer payload classes): for each declared
4659
4614
  // `impureCapability` capability, a typed Obligation/Result/Failure + handler typedef + dispatcher.
4660
4615
  // ADDITIVE + omit-when-empty: a domain with no `impureCapabilities` emits nothing (and the
@@ -4696,6 +4651,7 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
4696
4651
  combinedsByType.get(a.id) ?? [],
4697
4652
  refIdClassForAggregate,
4698
4653
  ownIdFieldsByAggregate.get(a.id) ?? new Set(),
4654
+ aggregateWireTypeForAggregate,
4699
4655
  ),
4700
4656
  )
4701
4657
  .join("\n\n");
@@ -4821,7 +4777,7 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
4821
4777
  `// Regenerate: \`npx tsx src/emit_dart.ts\` from ts_packages/co2-nomos-domains.`,
4822
4778
  `//`,
4823
4779
  `// ignore_for_file: unnecessary_this, prefer_const_constructors, constant_identifier_names`,
4824
- `// ignore_for_file: unused_import`,
4780
+ `// ignore_for_file: unused_import, invalid_use_of_internal_member`,
4825
4781
  ``,
4826
4782
  // A nullable plain-`t.json()` read-model slot decode re-stringifies via `jsonEncode`.
4827
4783
  ...(needsJsonConvert ? [`import 'dart:convert';`] : []),
@@ -4830,7 +4786,7 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
4830
4786
  // `../wire.dart` path into the framework (which broke when the generated dir moved
4831
4787
  // out of nomos_client into the co-located tenant package). The framework barrel
4832
4788
  // re-exports wire/dispatch/subscriptions/provider, so one import covers them all.
4833
- `import 'package:nomos_client/nomos_client.dart';`,
4789
+ `import 'package:nomos_client/src/nms_generated_runtime.dart';`,
4834
4790
  // FRAMEWORK-SHARED aggregates (RelationTuple, …) are emitted ONCE per package in the shared
4835
4791
  // file; import it so this domain's read accessors / ref-id fields resolve those symbols.
4836
4792
  ...(opts.frameworkImportUri !== undefined ? [`import '${opts.frameworkImportUri}';`] : []),
@@ -4887,13 +4843,6 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
4887
4843
  `// ---- public intents (typed DTOs; ship via the generic offer() verb) ----`,
4888
4844
  dirBlock,
4889
4845
  ``,
4890
- ...(mintedBlock !== ""
4891
- ? [
4892
- `// ---- typed ids + minted creates (kernel id-mint: per-aggregate typed id classes + create… factories) ----`,
4893
- mintedBlock,
4894
- ``,
4895
- ]
4896
- : []),
4897
4846
  `// ---- read accessors (typed subscriptions over the GitHolon query()/query_by_id() surface) ----`,
4898
4847
  readBlock,
4899
4848
  ``,