@githolon/dsl 0.108.0 → 0.108.2

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 (95) hide show
  1. package/dart/CHANGELOG.md +5 -0
  2. package/dart/lib/src/application_framework_calls.dart +299 -0
  3. package/dart/lib/src/generated_call.dart +96 -0
  4. package/dart/lib/src/nms_generated_runtime.dart +29 -0
  5. package/dart/lib/src/node_runner_transport.dart +77 -1
  6. package/dart/lib/src/result_usda.dart +487 -0
  7. package/dart/lib/src/runner/node-entry.mjs +6401 -2102
  8. package/dart/lib/src/runner/offline-install.mjs +1538 -163
  9. package/dart/lib/src/runtime_v2_gateway.dart +54 -0
  10. package/dart/lib/src/runtime_v2_transport.dart +117 -0
  11. package/dart/lib/src/session_bound.dart +7 -2
  12. package/dart/lib/src/test_harness.dart +193 -112
  13. package/dart/lib/src/webview_bridge.dart +2120 -569
  14. package/dart/pubspec.yaml +2 -2
  15. package/dart/test/acquire_session_credentials_test.dart +1 -1
  16. package/dart/test/acquire_session_test.dart +68 -14
  17. package/dart/test/adv_realm_interference_test.dart +6 -4
  18. package/dart/test/cluster_births_test.dart +11 -8
  19. package/dart/test/cluster_local_autosync_test.dart +1 -1
  20. package/dart/test/cluster_scale_test.dart +3 -2
  21. package/dart/test/dlq_session_surface_test.dart +52 -35
  22. package/dart/test/first_app_gauntlet_test.dart +1 -1
  23. package/dart/test/local_sync_inert_test.dart +23 -26
  24. package/dart/test/offer_authentication_proof_test.dart +193 -0
  25. package/dart/test/result_usda_test.dart +49 -0
  26. package/dart/test/session_bound_test.dart +8 -11
  27. package/dart/test/stale_bundle_test.dart +18 -3
  28. package/dart/test/test_harness_test.dart +14 -27
  29. package/dart/test/webview_bridge_test.dart +504 -83
  30. package/dart_types/CHANGELOG.md +7 -0
  31. package/dart_types/lib/nomos_kernel_protocol.dart +3 -2
  32. package/dart_types/lib/src/custody.dart +64 -19
  33. package/dart_types/lib/src/local_work_rejected.dart +30 -45
  34. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pb.dart +169 -0
  35. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pbenum.dart +11 -0
  36. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +2021 -234
  37. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +175 -8
  38. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +8664 -0
  39. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pbenum.dart +369 -0
  40. package/dart_types/lib/src/surfaced.dart +30 -25
  41. package/dart_types/pubspec.yaml +1 -1
  42. package/dart_types/test/surfaced_test.dart +2 -1
  43. package/package.json +1 -1
  44. package/runtime/generated-application-host.js +5736 -2047
  45. package/src/authoring.ts +3 -2
  46. package/src/births.ts +21 -0
  47. package/src/build_package.ts +30 -0
  48. package/src/business_birth.ts +39 -35
  49. package/src/codegen_call_usda.ts +1111 -0
  50. package/src/codegen_dart.ts +207 -282
  51. package/src/codegen_dart_application.ts +195 -252
  52. package/src/codegen_dart_client.ts +1978 -593
  53. package/src/codegen_dart_framework_calls.ts +291 -0
  54. package/src/codegen_dart_scenario.ts +7 -0
  55. package/src/codegen_proof.ts +3 -4
  56. package/src/codegen_provider_dart.ts +21 -7
  57. package/src/codegen_query_usda.ts +214 -0
  58. package/src/codegen_result_usda.ts +685 -0
  59. package/src/codegen_ts.ts +3945 -1111
  60. package/src/compile_package_main.ts +360 -32
  61. package/src/directive.ts +19 -4
  62. package/src/engine_entry.ts +45 -4
  63. package/src/framework/application_home.ts +2 -0
  64. package/src/framework/auth_provider_law.ts +262 -20
  65. package/src/framework/home.ts +32 -28
  66. package/src/framework/owned_custody.ts +19 -11
  67. package/src/framework/owned_custody_value.ts +51 -0
  68. package/src/framework/relations.ts +57 -21
  69. package/src/framework/signing_identity.ts +34 -0
  70. package/src/framework/workspaces.ts +31 -9
  71. package/src/framework_call_definitions.ts +53 -0
  72. package/src/framework_read_definitions.ts +45 -0
  73. package/src/identity_frontier.ts +58 -0
  74. package/src/index.ts +20 -1
  75. package/src/intent.ts +216 -16
  76. package/src/law_check.ts +19 -1
  77. package/src/law_graph.ts +57 -8
  78. package/src/law_graph_usda.ts +73 -5
  79. package/src/law_value_type.ts +106 -19
  80. package/src/read.ts +47 -0
  81. package/src/read_projection_namespace.ts +7 -0
  82. package/src/shape_compat.ts +38 -39
  83. package/src/stable_ids.ts +37 -2
  84. package/src/stable_ids_for_module.ts +64 -4
  85. package/src/test_call_usda.ts +83 -0
  86. package/src/wire.ts +36 -2
  87. package/src/wire_encode.ts +45 -1
  88. package/src/workspace_sharing.ts +27 -10
  89. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbjson.dart +0 -6096
  90. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbserver.dart +0 -50
  91. package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pbjson.dart +0 -178
  92. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pb.dart +0 -828
  93. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pbenum.dart +0 -465
  94. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pbjson.dart +0 -573
  95. package/dart_types/lib/src/proto/nomos/runtime/v1/runtime.pbserver.dart +0 -50
@@ -7,7 +7,7 @@
7
7
 
8
8
  /**
9
9
  * Dart frontend codegen (DSL decision-6: the schema is the single source of
10
- * truth -> TS (done), proto (later), and Dart frontend types (here)).
10
+ * truth -> TypeScript, Protobuf host contracts, and Dart frontend types (here)).
11
11
  *
12
12
  * Emits, per domain:
13
13
  * - a Dart class per aggregate with its typed fields, parsed (`fromJson`) from
@@ -17,12 +17,10 @@
17
17
  * `watch…`/one-shot `read…` over the GitHolon `query()`/`query_by_id()`
18
18
  * surface (the read half of the offer/query boundary);
19
19
  * - a Dart enum per `t.enum([...])` field;
20
- * - a Dart intent class per domain action — a pure typed DTO. Each class exposes its
21
- * `domain` / `intentType` (so the call site never hand-types a string) and a
22
- * internal `nmsInternalToPayloadJson()` seam that serialises only its typed fields to data. It builds no
23
- * `Event` and no `Hlc`: the GitHolon executes the installed domain plan.
24
- * - a single generic effectful write verb [`offer`] that ships {domain, intentType,
25
- * nmsInternalToPayloadJson()} to the GitHolon `author(domain, intentType, payloadJson, actor)`.
20
+ * - a Dart input class per domain action — a pure typed DTO. It builds no `Event`,
21
+ * `Hlc`, generic map, or JSON representation: the companion generated client lowers
22
+ * its fields directly into the canonical typed USDA call.
23
+ * - one effectful write verb [`offer`] carrying those USDA bytes to the GitHolon.
26
24
  *
27
25
  * It does NOT hand-write the fixed wire types (`Value`/`Op`/`Event`/...): those
28
26
  * live hand-written in `dart_types/lib/src/wire.dart`. Only the per-domain shapes are
@@ -30,7 +28,7 @@
30
28
  */
31
29
  import { z } from "zod";
32
30
  import { enumTypeNameOf } from "./enum_type.js";
33
- import { voMetaOf, collectPayloadValueObjectDescriptors, idOfMetaOf, idOfTargetKindOf, refOfMetaOf, versionedRefOfMetaOf, brandedIdMetaOf, NOMOS_UNIT_BRAND } from "./value_object.js";
31
+ import { voMetaOf, collectPayloadValueObjectDescriptors, collectValueObjectDescriptors, idOfMetaOf, idOfTargetKindOf, refOfMetaOf, versionedRefOfMetaOf, brandedIdMetaOf, NOMOS_UNIT_BRAND } from "./value_object.js";
34
32
  import { recursiveTypeBaseName, zodUnwrapLazy } from "./zod_lazy.js";
35
33
  import type { AggregateHandle } from "./aggregate.js";
36
34
  import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
@@ -78,14 +76,18 @@ export function dartActionInputClassName(directive: AnyDirective): string {
78
76
 
79
77
  // ---- Zod payload introspection --------------------------------------------
80
78
 
81
- /** How a typed payload field serialises into the payload-as-DATA JSON the engine reads. */
82
- export type ToJsonWrap =
79
+ /** The concrete Dart representation of a typed payload field. */
80
+ export type DartPayloadShape =
83
81
  | "scalar" // String/int/bool — emit the field directly.
84
82
  | "enum" // generated enum — emit `.wire`.
85
83
  | "strList" // List<String> — emit directly.
86
- | "json" // nested object — emit the field's `Map<String, Object?>` directly.
87
- | "vo" // a declared value object — emit `.toJson()` (the generated VO class).
88
- | "voList" // a list of value objects — emit `.map((e) => e.toJson()).toList()`.
84
+ | "wrappedList" // List of nominal scalar wrappers — each element emits `.value`.
85
+ | "refList" // List<NomosRef<T>> — each element emits its canonical id.
86
+ | "record" // anonymous z.object — a structural Dart named record.
87
+ | "recordList" // list of anonymous z.object values — typed Dart records.
88
+ | "stringMap" // a declared z.record(z.string()) — Map<String, String>, encoded field-by-field.
89
+ | "vo" // a declared typed value object.
90
+ | "voList" // a list of declared typed value objects.
89
91
  | "wrapped" // a standalone branded-id or unit extension type — emit `.value`.
90
92
  | "ref" // an aggregate-specific NomosRef<T> — emit its `.id` wire scalar.
91
93
  | "addressRef" // a workspace-qualified NomosRef<T> — preserve its complete typed address.
@@ -107,22 +109,21 @@ export interface PayloadFieldSpec {
107
109
  versionedRefSeriesWireType?: string;
108
110
  /** for enums: the Dart enum type name + raw values. */
109
111
  enumValues?: string[];
110
- /** how this field serialises into `nmsInternalToPayloadJson()`. */
111
- toJson: ToJsonWrap;
112
+ /** Concrete generated Dart shape consumed directly by the USDA encoder. */
113
+ shape: DartPayloadShape;
112
114
  /** Wire scalar under a nominal wrapper. Keeps string semantics distinct from numeric units. */
113
115
  wrappedScalarKind?: "string" | "number";
114
116
  /**
115
117
  * Whether the payload field is OPTIONAL (Zod `.optional()`/`.default()`). Optional
116
- * fields are nullable in Dart and OMITTED from `nmsInternalToPayloadJson()` when null — so the
117
- * engine's `.plan()` sees exactly the present-field set its TS Zod schema would (it
118
- * only emits ops for present fields). Required fields are always emitted.
118
+ * fields are nullable in Dart and omitted from the generated USDA call when null, so
119
+ * the engine's `.plan()` sees exactly the present-field set its schema declares.
119
120
  */
120
121
  optional: boolean;
121
122
  /**
122
123
  * NULL SEMANTICS (null_semantics.md): the payload field ADMITS a domain `null` (its zod is
123
124
  * `.nullable()`, a `t.nullable(...)`/`t.clearable(...)` sink). It generates as the sealed
124
125
  * tri-state [NomosSlot] so the call site says OMITTED / EXPLICIT-NULL / PRESENT explicitly —
125
- * `nmsInternalToPayloadJson` then omits, writes a real `null`, or writes the value accordingly.
126
+ * the USDA encoder then omits, writes a real `null`, or writes the value accordingly.
126
127
  * `false`/absent for a plain field (byte-identical to a pre-feature payload). */
127
128
  nullAdmits?: boolean;
128
129
  }
@@ -159,7 +160,7 @@ export function dartForeignReferenceInputs(
159
160
  const outputs = aggregates.filter((aggregate) => outputIds.has(aggregate.id));
160
161
 
161
162
  return specs.flatMap((spec) => {
162
- if (spec.toJson !== "ref" || recordInputIds.has(spec.name)) return [];
163
+ if (spec.shape !== "ref" || recordInputIds.has(spec.name)) return [];
163
164
  const matches = outputs.flatMap((aggregate) => {
164
165
  const field = aggregate.fields[spec.name] as Field | undefined;
165
166
  if (
@@ -364,7 +365,7 @@ function isOptionalOrDefault(zt: z.ZodTypeAny): boolean {
364
365
  * engine's same Zod schema validates, so the call-site type can never truncate a
365
366
  * fractional value the engine would accept.
366
367
  */
367
- function zodNumberIsInt(zt: z.ZodTypeAny): boolean {
368
+ export function zodNumberIsInt(zt: z.ZodTypeAny): boolean {
368
369
  const raw = zt as unknown as { isInt?: boolean };
369
370
  if (raw.isInt === true) return true;
370
371
  const checks = zodDef(zt).checks ?? [];
@@ -401,6 +402,11 @@ export function introspectPayload(
401
402
  else optional = true;
402
403
  zt = zodInnerType(zt);
403
404
  }
405
+ // Literal fields are compiler-owned constants. The canonical USDA encoder emits
406
+ // them from the DSL graph, so exposing them as Dart constructor arguments would
407
+ // widen a literal constraint into an ordinary bool/string/number and ask callers
408
+ // to repeat information the model already knows.
409
+ if (zodTypeName(zt) === "ZodLiteral") continue;
404
410
  out.push({
405
411
  ...introspectField(name, zt, enumDartTypes, lazyNames),
406
412
  optional,
@@ -410,6 +416,20 @@ export function introspectPayload(
410
416
  return out;
411
417
  }
412
418
 
419
+ function dartAnonymousRecordType(
420
+ schema: z.ZodTypeAny,
421
+ enumDartTypes: Map<string, string>,
422
+ lazyNames?: Map<z.ZodTypeAny, string>,
423
+ ): string {
424
+ const fields = introspectPayload(schema, enumDartTypes, lazyNames).map((spec) => {
425
+ if (spec.nullAdmits === true) {
426
+ return `NomosSlot<${stripNullable(spec.dartType)}> ${dartMemberName(spec.name)}`;
427
+ }
428
+ return `${spec.dartType}${spec.optional ? "?" : ""} ${dartMemberName(spec.name)}`;
429
+ });
430
+ return `({${fields.join(", ")}})`;
431
+ }
432
+
413
433
  /**
414
434
  * The aggregate ids a directive set references via `idOf(Agg)` in its TOP-LEVEL payload fields
415
435
  * (dart_codegen_parity.md P2). A cross-domain `idOf` types the field as `NomosRef<SiblingAggregate>`;
@@ -441,8 +461,8 @@ export function collectPayloadIdOfTargets(directives: readonly { payloadSchema?:
441
461
 
442
462
  /** Introspect ONE (already-unwrapped) Zod field into its Dart spec. Handles the
443
463
  * four shapes a tenant domain uses: scalar (string/int/bool), enum, string[], and a
444
- * NESTED object (e.g. attach/detach's `target:{targetType,targetId}`) — the engine
445
- * receives nested objects as DATA, so we model them as `Map<String, Object?>`. */
464
+ * NESTED object (e.g. attach/detach's `target:{targetType,targetId}`), represented
465
+ * as a structural Dart named record rather than a generic map. */
446
466
  function introspectField(
447
467
  name: string,
448
468
  zt: z.ZodTypeAny,
@@ -458,7 +478,7 @@ function introspectField(
458
478
  name,
459
479
  dartType: `NomosRef<${target}>`,
460
480
  referenceType: addressed.aggregateType,
461
- toJson: "addressRef",
481
+ shape: "addressRef",
462
482
  };
463
483
  }
464
484
  const versioned = versionedRefOfMetaOf(zt);
@@ -472,7 +492,7 @@ function introspectField(
472
492
  return {
473
493
  name,
474
494
  dartType: `VersionedNomosRef<${target}, ${series}>`,
475
- toJson: "versionedRef",
495
+ shape: "versionedRef",
476
496
  versionedRefTarget: target,
477
497
  versionedRefSeries: series,
478
498
  versionedRefCurrentField: versioned.currentField,
@@ -487,32 +507,27 @@ function introspectField(
487
507
  };
488
508
  }
489
509
  if (recipientMetaOf(zt) !== undefined) {
490
- return { name, dartType: "Recipient<User>", toJson: "recipient" };
510
+ return { name, dartType: "Recipient<User>", shape: "recipient" };
491
511
  }
492
- // RECURSIVE `z.lazy` (a self-referencing tree/JSON payload): if the compiler synthesized a
493
- // typed recursive class for this knot (`lazyNames`), the field IS that class (toJson via its
494
- // `.toJson()`). Without a synthesized class (no map, or a degraded knot) it resolves one level
495
- // and flows into the opaque-JSON lane below (`Map<String,Object?>`) — the last-resort fallback.
512
+ // RECURSIVE `z.lazy`: the compiler must synthesize a typed recursive class for the knot.
513
+ // There is deliberately no Object?/JSON fallback on the authoring path.
496
514
  if (zodTypeName(zt) === "ZodLazy") {
497
515
  const cls = lazyNames?.get(zt);
498
- if (cls !== undefined) return { name, dartType: cls, toJson: "vo" };
499
- // No synthesized class (no map, or a DEGRADED knot — e.g. a lazy of a union): last-resort
500
- // opaque JSON. Resolve one level and reuse the normal lane, but any shape the lane can't
501
- // express (union, undeclared nested object) falls back to `Object?` rather than throwing.
516
+ if (cls !== undefined) return { name, dartType: cls, shape: "vo" };
517
+ // A recursive payload must have a generated nominal class. Refuse degraded knots instead
518
+ // of silently reopening an Object?/JSON escape hatch.
502
519
  const inner = resolveLazy(zt);
503
- if (zodTypeName(inner) === "ZodLazy") return { name, dartType: "Object?", toJson: "json" };
504
- try {
505
- return introspectField(name, inner, enumDartTypes, lazyNames);
506
- } catch {
507
- return { name, dartType: "Object?", toJson: "json" };
520
+ if (zodTypeName(inner) === "ZodLazy") {
521
+ throw new Error(`recursive payload field '${name}' has no generated Dart type`);
508
522
  }
523
+ return introspectField(name, inner, enumDartTypes, lazyNames);
509
524
  }
510
525
  // A DECLARED VO/union keeps its name regardless of its zod kind (a union(...) is a
511
526
  // z.discriminatedUnion, not a ZodObject — without this it would fall to the throw lane).
512
527
  {
513
528
  const vo = voMetaOf(zt);
514
529
  if (vo !== undefined) {
515
- return { name, dartType: vo.name, toJson: "vo" };
530
+ return { name, dartType: vo.name, shape: "vo" };
516
531
  }
517
532
  }
518
533
  const tn = zodTypeName(zt);
@@ -530,21 +545,21 @@ function introspectField(
530
545
  name,
531
546
  dartType: `NomosRef<${targetClass}>`,
532
547
  referenceType: idAgg,
533
- toJson: "ref",
548
+ shape: "ref",
534
549
  };
535
550
  }
536
551
  if (isSha256DigestSchema(zt)) {
537
- return { name, dartType: "Sha256Digest", toJson: "wrapped", wrappedScalarKind: "string" };
552
+ return { name, dartType: "Sha256Digest", shape: "wrapped", wrappedScalarKind: "string" };
538
553
  }
539
554
  if (isGitObjectIdSchema(zt)) {
540
- return { name, dartType: "GitObjectId", toJson: "wrapped", wrappedScalarKind: "string" };
555
+ return { name, dartType: "GitObjectId", shape: "wrapped", wrappedScalarKind: "string" };
541
556
  }
542
557
  // brandedId("SiteId"): a standalone nominal string → the generated extension type.
543
558
  const brand = brandedIdMetaOf(zt);
544
559
  if (brand !== undefined) {
545
- return { name, dartType: brand, toJson: "wrapped", wrappedScalarKind: "string" };
560
+ return { name, dartType: brand, shape: "wrapped", wrappedScalarKind: "string" };
546
561
  }
547
- return { name, dartType: "String", toJson: "scalar" };
562
+ return { name, dartType: "String", shape: "scalar" };
548
563
  }
549
564
  case "ZodNumber":
550
565
  // A `z.number()` is a FRACTIONAL double in JS (e.g. catalogues `createListing`
@@ -556,21 +571,21 @@ function introspectField(
556
571
  {
557
572
  const unitName = (zt as unknown as Record<symbol, unknown>)[NOMOS_UNIT_BRAND];
558
573
  if (typeof unitName === "string") {
559
- return { name, dartType: unitName, toJson: "wrapped", wrappedScalarKind: "number" };
574
+ return { name, dartType: unitName, shape: "wrapped", wrappedScalarKind: "number" };
560
575
  }
561
576
  }
562
577
  return {
563
578
  name,
564
579
  dartType: zodNumberIsInt(zt) ? "int" : "double",
565
- toJson: "scalar",
580
+ shape: "scalar",
566
581
  };
567
582
  case "ZodBoolean":
568
- return { name, dartType: "bool", toJson: "scalar" };
583
+ return { name, dartType: "bool", shape: "scalar" };
569
584
  case "ZodEnum": {
570
585
  const values = zodEnumValues(zt);
571
586
  const key = dartEnumLookupKey(zt);
572
587
  const dartType = enumDartTypes.get(key) ?? `${cap(name)}Enum`;
573
- return { name, dartType, toJson: "enum", enumValues: values };
588
+ return { name, dartType, shape: "enum", enumValues: values };
574
589
  }
575
590
  case "ZodArray": {
576
591
  const elem = zodArrayElement(zt);
@@ -578,18 +593,53 @@ function introspectField(
578
593
  // with the generated VO class (vo_first_class.md). ANY other element
579
594
  // type is arbitrary JSON DATA to the engine, modelled as `List<Object?>`.
580
595
  if (zodTypeName(elem) === "ZodString") {
581
- return { name, dartType: "List<String>", toJson: "strList" };
596
+ const idAgg = idOfMetaOf(elem);
597
+ if (idAgg !== undefined) {
598
+ const targetClass = idOfTargetKindOf(elem) === "contract"
599
+ ? contractTypeName(idAgg)
600
+ : concreteClassName(idAgg);
601
+ return {
602
+ name,
603
+ dartType: `List<NomosRef<${targetClass}>>`,
604
+ referenceType: idAgg,
605
+ shape: "refList",
606
+ };
607
+ }
608
+ if (isSha256DigestSchema(elem)) {
609
+ return { name, dartType: "List<Sha256Digest>", shape: "wrappedList", wrappedScalarKind: "string" };
610
+ }
611
+ if (isGitObjectIdSchema(elem)) {
612
+ return { name, dartType: "List<GitObjectId>", shape: "wrappedList", wrappedScalarKind: "string" };
613
+ }
614
+ const brand = brandedIdMetaOf(elem);
615
+ if (brand !== undefined) {
616
+ return { name, dartType: `List<${brand}>`, shape: "wrappedList", wrappedScalarKind: "string" };
617
+ }
618
+ return { name, dartType: "List<String>", shape: "strList" };
619
+ }
620
+ if (zodTypeName(elem) === "ZodNumber") {
621
+ const unitName = (elem as unknown as Record<symbol, unknown>)[NOMOS_UNIT_BRAND];
622
+ if (typeof unitName === "string") {
623
+ return { name, dartType: `List<${unitName}>`, shape: "wrappedList", wrappedScalarKind: "number" };
624
+ }
582
625
  }
583
626
  // A list of a RECURSIVE `z.lazy` knot (a synthesized class) is a typed `List<Class>`.
584
627
  const elemCls = zodTypeName(elem) === "ZodLazy" ? lazyNames?.get(elem) : undefined;
585
628
  if (elemCls !== undefined) {
586
- return { name, dartType: `List<${elemCls}>`, toJson: "voList" };
629
+ return { name, dartType: `List<${elemCls}>`, shape: "voList" };
587
630
  }
588
631
  const elemVo = voMetaOf(elem);
589
632
  if (elemVo !== undefined) {
590
- return { name, dartType: `List<${elemVo.name}>`, toJson: "voList" };
633
+ return { name, dartType: `List<${elemVo.name}>`, shape: "voList" };
591
634
  }
592
- return { name, dartType: "List<Object?>", toJson: "json" };
635
+ if (zodTypeName(elem) === "ZodObject") {
636
+ return {
637
+ name,
638
+ dartType: `List<${dartAnonymousRecordType(elem, enumDartTypes, lazyNames)}>`,
639
+ shape: "recordList",
640
+ };
641
+ }
642
+ throw new Error(`payload list '${name}' needs a generated concrete Dart element type`);
593
643
  }
594
644
  case "ZodObject": {
595
645
  // A DECLARED VALUE OBJECT keeps its name: the payload field is the generated VO class
@@ -597,24 +647,23 @@ function introspectField(
597
647
  // own `toJson()`. An anonymous `z.object(...)` stays the raw-map lane below.
598
648
  const vo = voMetaOf(zt);
599
649
  if (vo !== undefined) {
600
- return { name, dartType: vo.name, toJson: "vo" };
650
+ return { name, dartType: vo.name, shape: "vo" };
601
651
  }
602
- // Nested object payload (e.g. `target: z.object({targetType, targetId})`). The
603
- // engine receives it as DATA verbatim, so we model it as an arbitrary JSON map
604
- // — the call site builds the literal, and `toPayloadJson()` ships it through.
605
- return { name, dartType: "Map<String, Object?>", toJson: "json" };
652
+ return {
653
+ name,
654
+ dartType: dartAnonymousRecordType(zt, enumDartTypes, lazyNames),
655
+ shape: "record",
656
+ };
606
657
  }
607
658
  case "ZodRecord": {
608
659
  const valueType = zodRecordValueType(zt);
609
- const dartType =
610
- valueType !== undefined && zodTypeName(valueType) === "ZodString"
611
- ? "Map<String, String>"
612
- : "Map<String, Object?>";
613
- return { name, dartType, toJson: "json" };
660
+ if (valueType === undefined || zodTypeName(valueType) !== "ZodString") {
661
+ throw new Error(`payload map '${name}' needs a generated concrete Dart value type`);
662
+ }
663
+ return { name, dartType: "Map<String, String>", shape: "stringMap" };
614
664
  }
615
665
  case "ZodThing":
616
- // A `z.thing(...)` payload — also arbitrary JSON DATA to the engine.
617
- return { name, dartType: "Map<String, Object?>", toJson: "json" };
666
+ throw new Error(`payload field '${name}' is an unstructured thing; declare its typed schema`);
618
667
  default:
619
668
  throw new Error(`unsupported payload field type '${tn}' for '${name}'`);
620
669
  }
@@ -638,7 +687,7 @@ function directiveDoc(d: AnyDirective): string {
638
687
 
639
688
  type MaterializedFieldKind = Exclude<FieldKind, "hasMany">;
640
689
 
641
- interface AggFieldSpec {
690
+ export interface AggFieldSpec {
642
691
  /** Dart-safe field/property name exposed on generated classes. */
643
692
  name: string;
644
693
  /** Canonical wire/readmodel key declared by the DSL, e.g. `spec.domainHash`. */
@@ -879,7 +928,7 @@ function dartImportLine(imp: DartImport): string {
879
928
  return `import '${imp.uri}'${prefix};`;
880
929
  }
881
930
 
882
- function refIdClassResolver(mod: DomainModule): (aggregateType: string) => string {
931
+ export function refIdClassResolver(mod: DomainModule): (aggregateType: string) => string {
883
932
  const imports = new Map<string, DartRefImport>();
884
933
  for (const imp of mod.dartRefImports ?? []) {
885
934
  imports.set(imp.aggregateType, imp);
@@ -1164,7 +1213,7 @@ function aggFieldSpec(
1164
1213
  }
1165
1214
  }
1166
1215
 
1167
- function aggregateFieldSpecs(
1216
+ export function aggregateFieldSpecs(
1168
1217
  agg: AggregateHandle,
1169
1218
  enumDartTypes: Map<string, string>,
1170
1219
  refIdClassForAggregate: (aggregateType: string) => string = concreteClassName,
@@ -1381,21 +1430,31 @@ export function collectDartEnums(
1381
1430
  }
1382
1431
  }
1383
1432
  for (const d of directives) {
1384
- if (zodTypeName(d.payloadSchema as z.ZodTypeAny) !== "ZodObject") continue;
1385
- const shape = zodObjectShape(d.payloadSchema as z.ZodTypeAny);
1386
- for (const [name, zt] of Object.entries(shape)) {
1387
- const inner = unwrapZod(zt as z.ZodTypeAny);
1388
- if (zodTypeName(inner) === "ZodEnum") {
1433
+ const walkPayloadEnums = (schema: z.ZodTypeAny, path: readonly string[]): void => {
1434
+ const inner = unwrapZod(schema);
1435
+ const kind = zodTypeName(inner);
1436
+ if (kind === "ZodEnum") {
1389
1437
  const declaredName = enumTypeNameOf(inner);
1390
- if ((declaredName !== undefined) !== namedPass) continue;
1438
+ if ((declaredName !== undefined) !== namedPass) return;
1439
+ const fieldName = path.at(-1) ?? d.id;
1391
1440
  register(
1392
- name,
1441
+ fieldName,
1393
1442
  zodEnumValues(inner),
1394
- (d as { aggregateId?: string }).aggregateId ?? d.id,
1443
+ `${(d as { aggregateId?: string }).aggregateId ?? d.id}${path.length > 1 ? cap(path.slice(0, -1).join("_")) : ""}`,
1395
1444
  declaredName,
1396
1445
  );
1446
+ return;
1397
1447
  }
1398
- }
1448
+ if (kind === "ZodArray") {
1449
+ walkPayloadEnums(zodArrayElement(inner), path);
1450
+ return;
1451
+ }
1452
+ if (kind !== "ZodObject") return;
1453
+ for (const [name, child] of Object.entries(zodObjectShape(inner))) {
1454
+ walkPayloadEnums(child as z.ZodTypeAny, [...path, name]);
1455
+ }
1456
+ };
1457
+ walkPayloadEnums(d.payloadSchema as z.ZodTypeAny, []);
1399
1458
  }
1400
1459
  }
1401
1460
  for (const declaration of deriveds) {
@@ -1419,7 +1478,7 @@ export function dartEnumTypeForField(
1419
1478
  return dartTypes.get(dartEnumLookupKey(unwrapZod(field.zod)));
1420
1479
  }
1421
1480
 
1422
- function dartEnumLookupKey(
1481
+ export function dartEnumLookupKey(
1423
1482
  schemaOrValues: z.ZodTypeAny | readonly string[],
1424
1483
  declaredName?: string,
1425
1484
  ): string {
@@ -1969,7 +2028,7 @@ function voEnumFields(d: ValueObjectDescriptor): {
1969
2028
  }
1970
2029
 
1971
2030
  /** The generated arm class name of one union arm (`Shape` + `circle` → `ShapeCircle`). */
1972
- function unionArmClassName(unionName: string, armKey: string): string {
2031
+ export function unionArmClassName(unionName: string, armKey: string): string {
1973
2032
  return `${unionName}${armKey[0]!.toUpperCase()}${armKey.slice(1)}`;
1974
2033
  }
1975
2034
 
@@ -2073,12 +2132,16 @@ function voToJsonExpr(f: ValueObjectFieldDescriptor, expr: string, depth = 0): s
2073
2132
 
2074
2133
  /** Deep json equality/hash helpers shared by every generated VO class — emitted ONCE
2075
2134
  * per file when the file emits ≥1 VO class (`toJson()`-based value equality). */
2076
- function emitVoHelpers(): string {
2135
+ function emitVoHelpers(includeCopyWithSentinel: boolean): string {
2077
2136
  return [
2078
- `/// The copyWith ABSENT-vs-null sentinel: an omitted optional param keeps the current`,
2079
- `/// value; an explicit \`null\` clears it. (Dart cannot distinguish the two otherwise.)`,
2080
- `const Object _voUnset = Object();`,
2081
- ``,
2137
+ ...(includeCopyWithSentinel
2138
+ ? [
2139
+ `/// The copyWith ABSENT-vs-null sentinel: an omitted optional param keeps the current`,
2140
+ `/// value; an explicit \`null\` clears it. (Dart cannot distinguish the two otherwise.)`,
2141
+ `const Object _voUnset = Object();`,
2142
+ ``,
2143
+ ]
2144
+ : []),
2082
2145
  `/// Deep json equality for generated value-object classes (toJson-based).`,
2083
2146
  `bool _voJsonEq(Object? a, Object? b) {`,
2084
2147
  ` if (a is Map && b is Map) {`,
@@ -3753,15 +3816,11 @@ function emitDirective(
3753
3816
  const specs = introspectPayload(d.payloadSchema as z.ZodTypeAny, enumDartTypes, lazyNames);
3754
3817
  const recordInputs = dartBusinessRecordInputs(d, aggregates);
3755
3818
  const foreignReferenceInputs = dartForeignReferenceInputs(d, aggregates);
3756
- const foreignReferenceIds = new Set(foreignReferenceInputs.map((input) => input.idField));
3757
3819
  const principalField = d.businessPrincipalField;
3758
3820
  // targetBinding is declared on AnyDirective — honour it everywhere (client method,
3759
3821
  // payload class, application surface) or the three emitters disagree and the
3760
3822
  // generated Dart cannot compile.
3761
3823
  const targetBinding = (d as { targetBinding?: import("./directive.js").DirectiveTargetBinding }).targetBinding;
3762
- const targetType = targetBinding === undefined
3763
- ? undefined
3764
- : `NomosRef<${concreteClassName(targetBinding.aggregateId)}>`;
3765
3824
  const derivedFields = new Set([
3766
3825
  ...recordInputs.flatMap((input) => [input.idField, input.workspaceField]),
3767
3826
  ...foreignReferenceInputs.map((input) => input.workspaceField),
@@ -3774,33 +3833,18 @@ function emitDirective(
3774
3833
  // makes an accidentally forgotten relationship indistinguishable from a
3775
3834
  // deliberate omission. The generated sum type keeps those meanings apart.
3776
3835
  const isOptionalReferenceChoice = (spec: PayloadFieldSpec): boolean =>
3777
- spec.optional && spec.toJson === "ref";
3778
- const internalPayloadArguments = [
3779
- ...(targetBinding === undefined
3780
- ? []
3781
- : [`required ${targetType} nmsInternalTarget`]),
3782
- ...(principalField === undefined ? [] : ["required String nmsInternalPrincipal"]),
3783
- ...(recordInputs.length === 0
3784
- ? []
3785
- : [
3786
- "required String Function(Object record) nmsInternalLocationOf",
3787
- "required String Function(Object record) nmsInternalIdOf",
3788
- ]),
3789
- ];
3790
- const internalPayloadSignature = internalPayloadArguments.length === 0
3791
- ? ""
3792
- : `{${internalPayloadArguments.join(", ")}}`;
3836
+ spec.optional && spec.shape === "ref";
3793
3837
 
3794
- // Field decls: optional payload fields are nullable Dart fields (omitted from the
3795
- // payload JSON when null); required fields are non-null + `required` in the ctor.
3838
+ // Field declarations stay fully typed; optional fields are omitted directly by
3839
+ // the USDA encoder while required fields remain non-null constructor inputs.
3796
3840
  const fieldDecls = [
3797
3841
  ...publicSpecs
3798
3842
  .map((s) =>
3799
3843
  s.nullAdmits === true
3800
- ? ` final NomosSlot<${stripNullable(s.dartType)}> ${s.name};`
3844
+ ? ` final NomosSlot<${stripNullable(s.dartType)}> ${dartMemberName(s.name)};`
3801
3845
  : isOptionalReferenceChoice(s)
3802
- ? ` final NomosOptionalRef<${s.dartType.replace(/^NomosRef<(.*)>$/, "$1")}> ${s.name};`
3803
- : ` final ${s.dartType}${s.optional ? "?" : ""} ${s.name};`,
3846
+ ? ` final NomosOptionalRef<${s.dartType.replace(/^NomosRef<(.*)>$/, "$1")}> ${dartMemberName(s.name)};`
3847
+ : ` final ${s.dartType}${s.optional ? "?" : ""} ${dartMemberName(s.name)};`,
3804
3848
  ),
3805
3849
  ...recordInputs.map((input) => ` final ${input.dartType} ${input.argument};`),
3806
3850
  ].join("\n");
@@ -3810,10 +3854,10 @@ function emitDirective(
3810
3854
  // A null-admitting slot defaults to Omitted (the call site chooses Present/ExplicitNull);
3811
3855
  // an optional field defaults to null; a required field is `required`.
3812
3856
  s.nullAdmits === true
3813
- ? ` this.${s.name} = const NomosSlot.omitted(),`
3857
+ ? ` this.${dartMemberName(s.name)} = const NomosSlot.omitted(),`
3814
3858
  : isOptionalReferenceChoice(s)
3815
- ? ` required this.${s.name},`
3816
- : ` ${s.optional ? "" : "required "}this.${s.name},`,
3859
+ ? ` required this.${dartMemberName(s.name)},`
3860
+ : ` ${s.optional ? "" : "required "}this.${dartMemberName(s.name)},`,
3817
3861
  ),
3818
3862
  ...recordInputs.map((input) => ` required this.${input.argument},`),
3819
3863
  ].join("\n");
@@ -3825,90 +3869,6 @@ function emitDirective(
3825
3869
  ? [` const ${className}({`, ctorParams, ` });`]
3826
3870
  : [` const ${className}();`];
3827
3871
 
3828
- // toPayloadJson: serialise only typed fields to data. No Event, no Hlc; the
3829
- // GitHolon executes the installed domain plan. Optional
3830
- // fields are written only when present (mirrors the engine's Zod: ops are emitted
3831
- // only for present fields).
3832
- const requiredEntries = specs
3833
- .filter((s) =>
3834
- !derivedFields.has(s.name) &&
3835
- !foreignReferenceIds.has(s.name) &&
3836
- !s.optional &&
3837
- s.nullAdmits !== true
3838
- )
3839
- .map((s) => ` '${s.name}': ${toJsonExpr(s, s.name)},`);
3840
- const targetEntry = targetBinding === undefined
3841
- ? []
3842
- : [` '${targetBinding.field}': NmsInternalNomosRefs.id(nmsInternalTarget),`];
3843
- const optionalLines = specs
3844
- .filter((s) =>
3845
- !derivedFields.has(s.name) &&
3846
- s.optional &&
3847
- s.nullAdmits !== true &&
3848
- !isOptionalReferenceChoice(s)
3849
- )
3850
- .map(
3851
- (s) =>
3852
- ` if (${s.name} != null) '${s.name}': ${toJsonExpr(s, `${s.name}!`)},`,
3853
- );
3854
- // NULL SEMANTICS: a null-admitting field writes through its slot — Omitted writes nothing,
3855
- // ExplicitNull writes a real `null`, Present writes the encoded value (null_semantics.md).
3856
- const slotSpecs = specs.filter((s) => !derivedFields.has(s.name) && s.nullAdmits === true);
3857
- const slotWriteLines = slotSpecs.map(
3858
- (s) => ` ${s.name}.writeTo(data, '${s.name}', (v) => ${toJsonExpr(s, "v")});`,
3859
- );
3860
- const optionalReferenceSpecs = specs.filter(
3861
- (s) =>
3862
- !derivedFields.has(s.name) &&
3863
- !foreignReferenceIds.has(s.name) &&
3864
- isOptionalReferenceChoice(s),
3865
- );
3866
- const optionalReferenceWriteLines = optionalReferenceSpecs.map(
3867
- (s) => ` ${s.name}.writeTo(data, '${s.name}', (reference) => NmsInternalNomosRefs.id(reference));`,
3868
- );
3869
- const requiredForeignReferenceInputs = foreignReferenceInputs.filter((input) => !input.optional);
3870
- const requiredWorkspaceSources = new Map<string, DartForeignReferenceInput>();
3871
- const requiredForeignReferenceEntries = requiredForeignReferenceInputs.flatMap((input) => {
3872
- const first = requiredWorkspaceSources.get(input.workspaceField);
3873
- if (first === undefined) requiredWorkspaceSources.set(input.workspaceField, input);
3874
- return [
3875
- ...(first === undefined
3876
- ? [` '${input.workspaceField}': NmsInternalNomosRefs.workspace(${input.idField}),`]
3877
- : []),
3878
- ` '${input.idField}': NmsInternalNomosRefs.id(${input.idField}),`,
3879
- ];
3880
- });
3881
- const requiredForeignReferenceValidationLines = requiredForeignReferenceInputs.flatMap((input) => {
3882
- const first = requiredWorkspaceSources.get(input.workspaceField)!;
3883
- if (first.idField === input.idField) return [];
3884
- return [
3885
- ` if (NmsInternalNomosRefs.workspace(${input.idField}) != NmsInternalNomosRefs.workspace(${first.idField})) {`,
3886
- ` throw ArgumentError('Foreign references for ${input.workspaceField} must belong to the same workspace.');`,
3887
- ` }`,
3888
- ];
3889
- });
3890
- const optionalForeignReferenceWriteLines = foreignReferenceInputs
3891
- .filter((input) => input.optional)
3892
- .flatMap((input) => [
3893
- ` ${input.idField}.when(`,
3894
- ` absent: () {},`,
3895
- ` present: (reference) {`,
3896
- ` final workspace = NmsInternalNomosRefs.workspace(reference);`,
3897
- ` final existingWorkspace = data['${input.workspaceField}'];`,
3898
- ` if (existingWorkspace != null && existingWorkspace != workspace) {`,
3899
- ` throw ArgumentError('Foreign references for ${input.workspaceField} must belong to the same workspace.');`,
3900
- ` }`,
3901
- ` data['${input.workspaceField}'] = workspace;`,
3902
- ` data['${input.idField}'] = NmsInternalNomosRefs.id(reference);`,
3903
- ` },`,
3904
- ` );`,
3905
- ]);
3906
- const hasPostWrites =
3907
- slotSpecs.length > 0 ||
3908
- optionalReferenceSpecs.length > 0 ||
3909
- requiredForeignReferenceValidationLines.length > 0 ||
3910
- optionalForeignReferenceWriteLines.length > 0;
3911
-
3912
3872
  const docFirst = directiveDoc(d);
3913
3873
 
3914
3874
  return [
@@ -3923,72 +3883,10 @@ function emitDirective(
3923
3883
  ` /// ${docFirst}`,
3924
3884
  ...ctorLines,
3925
3885
  ``,
3926
- ` /// Nomos implementation seam used only by the generated application package.`,
3927
- ` /// Tenant code should pass this object to the generated domain application instead.`,
3928
- ...(hasPostWrites
3929
- ? [
3930
- ` Map<String, Object?> nmsInternalToPayloadJson(${internalPayloadSignature}) {`,
3931
- ` final data = <String, Object?>{`,
3932
- ...requiredEntries,
3933
- ...targetEntry,
3934
- ...requiredForeignReferenceEntries,
3935
- ...optionalLines,
3936
- ...(principalField === undefined ? [] : [` '${principalField}': nmsInternalPrincipal,`]),
3937
- ...recordInputs.flatMap((input) => [
3938
- ` '${input.workspaceField}': nmsInternalLocationOf(${input.argument}),`,
3939
- ` '${input.idField}': nmsInternalIdOf(${input.argument}),`,
3940
- ]),
3941
- ` };`,
3942
- ...requiredForeignReferenceValidationLines,
3943
- ...slotWriteLines,
3944
- ...optionalReferenceWriteLines,
3945
- ...optionalForeignReferenceWriteLines,
3946
- ` return data;`,
3947
- ` }`,
3948
- ]
3949
- : [
3950
- ` Map<String, Object?> nmsInternalToPayloadJson(${internalPayloadSignature}) => <String, Object?>{`,
3951
- ...requiredEntries,
3952
- ...targetEntry,
3953
- ...requiredForeignReferenceEntries,
3954
- ...optionalLines,
3955
- ...(principalField === undefined ? [] : [` '${principalField}': nmsInternalPrincipal,`]),
3956
- ...recordInputs.flatMap((input) => [
3957
- ` '${input.workspaceField}': nmsInternalLocationOf(${input.argument}),`,
3958
- ` '${input.idField}': nmsInternalIdOf(${input.argument}),`,
3959
- ]),
3960
- ` };`,
3961
- ]),
3962
3886
  `}`,
3963
3887
  ].join("\n");
3964
3888
  }
3965
3889
 
3966
- /** Expression that serialises ONE typed payload field into its JSON value. */
3967
- export function toJsonExpr(spec: PayloadFieldSpec, accessor: string): string {
3968
- switch (spec.toJson) {
3969
- case "scalar":
3970
- case "strList":
3971
- case "json":
3972
- return accessor;
3973
- case "enum":
3974
- return `${accessor}.wire`;
3975
- case "vo":
3976
- return `${accessor}.toJson()`;
3977
- case "voList":
3978
- return `${accessor}.map((e) => e.toJson()).toList()`;
3979
- case "wrapped":
3980
- return `${accessor}.value`;
3981
- case "ref":
3982
- return `NmsInternalNomosRefs.id(${accessor})`;
3983
- case "addressRef":
3984
- return `NmsInternalNomosRefs.toJson(${accessor})`;
3985
- case "versionedRef":
3986
- return `{'pinned': NmsInternalNomosRefs.toJson(${accessor}.pinned), 'series': NmsInternalNomosRefs.toJson(${accessor}.series)}`;
3987
- case "recipient":
3988
- return `NmsInternalRecipients.address(${accessor})`;
3989
- }
3990
- }
3991
-
3992
3890
  // ── RECURSIVE `z.lazy` payloads → synthesized Dart classes (full support) ─────────
3993
3891
  //
3994
3892
  // A self-referencing payload schema (`TreeNode = z.lazy(() => z.object({ label, children:
@@ -4714,7 +4612,7 @@ function sampleFromZod(zt: z.ZodTypeAny): unknown {
4714
4612
  }
4715
4613
  }
4716
4614
 
4717
- function collectOwnIdFieldsByAggregate(
4615
+ export function collectOwnIdFieldsByAggregate(
4718
4616
  aggregates: AggregateHandle[],
4719
4617
  directives: AnyDirective[],
4720
4618
  enumDartTypes: Map<string, string>,
@@ -5060,6 +4958,14 @@ export interface GenerateDartDomainOptions {
5060
4958
  * module carries locally.
5061
4959
  */
5062
4960
  readonly skipValueObjectNames?: ReadonlySet<string>;
4961
+ /**
4962
+ * Additional compiler-proven value-object closure to emit in this file. The package
4963
+ * composer uses this only for its synthetic shared types layer: ordinary domains
4964
+ * still derive their public vocabulary from public aggregates and payloads, so a
4965
+ * private framework aggregate cannot leak a nominal type into tenant Dart merely
4966
+ * because it was present in the composed law.
4967
+ */
4968
+ readonly additionalValueObjects?: readonly ValueObjectDescriptor[];
5063
4969
  }
5064
4970
 
5065
4971
  /** Generate the full Dart source for one domain module. */
@@ -5068,9 +4974,31 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
5068
4974
  (a) => (a as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
5069
4975
  );
5070
4976
  const publicAggregateIds = new Set(publicAggregates.map((a) => a.id));
4977
+ // Recompute the value vocabulary after removing framework-internal aggregates.
4978
+ // `mod.valueObjects` describes the whole composed law, so carrying it through
4979
+ // here would publish private framework values merely because they coexist with
4980
+ // the tenant domain. Payload-only public values are collected below from the
4981
+ // public directives, preserving every type reachable from the application API.
4982
+ const publicAggregateValueObjectsByName = new Map<string, ValueObjectDescriptor>();
4983
+ for (const descriptor of [
4984
+ ...collectValueObjectDescriptors(publicAggregates),
4985
+ ...(opts.additionalValueObjects ?? []),
4986
+ ]) {
4987
+ const prior = publicAggregateValueObjectsByName.get(descriptor.name);
4988
+ if (prior !== undefined && JSON.stringify(prior) !== JSON.stringify(descriptor)) {
4989
+ throw new Error(
4990
+ `value object '${descriptor.name}' has DIVERGENT public/shared Dart shapes — one name, one shape`,
4991
+ );
4992
+ }
4993
+ publicAggregateValueObjectsByName.set(descriptor.name, descriptor);
4994
+ }
4995
+ const publicAggregateValueObjects = [...publicAggregateValueObjectsByName.values()].sort(
4996
+ (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0),
4997
+ );
5071
4998
  mod = {
5072
4999
  ...mod,
5073
5000
  aggregates: publicAggregates,
5001
+ valueObjects: publicAggregateValueObjects,
5074
5002
  directives: (mod.applicationIntents ?? mod.directives).filter(
5075
5003
  (d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
5076
5004
  ),
@@ -5149,7 +5077,7 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
5149
5077
  try {
5150
5078
  for (const spec of introspectPayload(schema as z.ZodTypeAny, new Map())) {
5151
5079
  // A wrapped spec whose brand a VO descriptor already registered is a brandedId, not a unit.
5152
- if (spec.toJson === "wrapped" && spec.wrappedScalarKind === "number") {
5080
+ if (spec.shape === "wrapped" && spec.wrappedScalarKind === "number") {
5153
5081
  unitNames.add(spec.dartType);
5154
5082
  }
5155
5083
  }
@@ -5178,12 +5106,18 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
5178
5106
  `/// passing a bare String (or another brand) where [${b}] is required is a COMPILE error.`,
5179
5107
  `extension type const ${b}(String value) {}`,
5180
5108
  ].join("\n")).join("\n\n");
5109
+ const needsVoCopyWithSentinel = localValueObjects.some((descriptor) =>
5110
+ Object.values(descriptor.fields ?? {}).some((field) => field.optional === true)
5111
+ || Object.values(descriptor.union ?? {}).some((arm) =>
5112
+ "fields" in arm && Object.values(arm.fields).some((field) => field.optional === true),
5113
+ )
5114
+ );
5181
5115
  const voBlock = [
5182
5116
  ...(unitBlock !== "" ? [unitBlock] : []),
5183
5117
  ...(brandBlock !== "" ? [brandBlock] : []),
5184
5118
  ...(localValueObjects.length > 0
5185
5119
  ? [
5186
- emitVoHelpers(),
5120
+ emitVoHelpers(needsVoCopyWithSentinel),
5187
5121
  ...localValueObjects.flatMap((d) => [
5188
5122
  ...voEnumFields(d)
5189
5123
  .filter((e) => e.declaredName === undefined)
@@ -5501,16 +5435,14 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
5501
5435
  ]
5502
5436
  : []),
5503
5437
  ...helperBlock,
5504
- // NOTE: role UI-hint helpers (canEdit/canView) are emitted into the CLIENT package by dartRoleHelpers,
5505
- // NOT here — they need dart:convert, which the types-package purity gate forbids.
5438
+ // NOTE: role UI-hint helpers (canEdit/canView) are emitted into the CLIENT package by dartRoleHelpers.
5506
5439
  ].join("\n");
5507
5440
  }
5508
5441
 
5509
5442
  /** The role UI-hint helpers (`canEdit<Agg>`/`canView<Agg>` + `_holdsAnyRelation`) for the CLIENT package — a
5510
- * pure client-side MIRROR of the kernel ReBAC check over the folded RelationTuple projection. INFORMATIONAL
5511
- * only (the one wasm gate is the authority and refuses regardless). Uses `dart:convert` (jsonDecode of the
5512
- * per-subject leaf), so it may NOT live in the pure types file — the caller emits it into `<name>.client.dart`
5513
- * and ensures a `dart:convert` import. Returns "" when the domain declares no roles (era-safe, no import). */
5443
+ * pure client-side mirror of the kernel ReBAC check over the compiler-generated RelationTuple projection.
5444
+ * Every tuple and subject is nominally typed from the same DSL graph; there is no JSON leaf or map-shaped
5445
+ * boundary. INFORMATIONAL only (the one wasm gate is still authoritative). */
5514
5446
  export function dartRoleHelpers(mod: DomainModule): string {
5515
5447
  const editRels = new Map<string, Set<string>>();
5516
5448
  const viewRels = new Map<string, Set<string>>();
@@ -5525,21 +5457,14 @@ export function dartRoleHelpers(mod: DomainModule): string {
5525
5457
  if (editRels.size === 0 && viewRels.size === 0) return "";
5526
5458
  const lines: string[] = [
5527
5459
  `/// True iff [principal] holds ANY of [relations] as an ACTIVE tuple in [rows] (or a \`user:*\` wildcard).`,
5528
- `bool _holdsAnyRelation(List<Map<String, dynamic>>? rows, Set<String> relations, String principal) {`,
5460
+ `bool _holdsAnyRelation(List<RelationTupleReadModel>? rows, Set<String> relations, String principal) {`,
5529
5461
  ` for (final r in rows ?? const []) {`,
5530
- ` final data = r['data'];`,
5531
- ` if (data is! Map) continue;`,
5532
- ` final rel = data['relation'];`,
5533
- ` if (rel is! String || !relations.contains(rel)) continue;`,
5534
- ` final subjects = data['subjects'];`,
5535
- ` if (subjects is! Map) continue;`,
5536
- ` for (final entry in subjects.entries) {`,
5537
- ` final subj = entry.key.toString();`,
5538
- ` dynamic leaf = entry.value;`,
5539
- ` if (leaf is String) { try { leaf = jsonDecode(leaf); } catch (_) { leaf = const {}; } }`,
5540
- ` final status = leaf is Map ? leaf['status'] : null;`,
5541
- ` if (status != null && status != 'active') continue;`,
5462
+ ` if (r.relation case final relation? when relations.contains(relation)) {`,
5463
+ ` for (final entry in r.subjects.entries) {`,
5464
+ ` final subj = entry.key;`,
5465
+ ` if (entry.value.status != RelationshipSubjectStatus.active) continue;`,
5542
5466
  ` if (subj == 'user:\$principal' || subj == 'user:*') return true;`,
5467
+ ` }`,
5543
5468
  ` }`,
5544
5469
  ` }`,
5545
5470
  ` return false;`,
@@ -5550,14 +5475,14 @@ export function dartRoleHelpers(mod: DomainModule): string {
5550
5475
  lines.push(
5551
5476
  `/// UI HINT — may [principal] EDIT a ${agg}? Mirrors the kernel's ReBAC check; INFORMATIONAL only`,
5552
5477
  `/// (the one wasm gate is the authority and refuses regardless).`,
5553
- `bool canEdit${cap(agg)}(List<Map<String, dynamic>>? relations, String principal) =>`,
5478
+ `bool canEdit${cap(agg)}(List<RelationTupleReadModel>? relations, String principal) =>`,
5554
5479
  ` _holdsAnyRelation(relations, ${dartSet(rels)}, principal);`,
5555
5480
  );
5556
5481
  }
5557
5482
  for (const [agg, rels] of [...viewRels].sort((a, b) => (a[0] < b[0] ? -1 : 1))) {
5558
5483
  lines.push(
5559
5484
  `/// UI HINT — may [principal] VIEW a ${agg}? Mirrors the kernel's ReBAC read gate; INFORMATIONAL only.`,
5560
- `bool canView${cap(agg)}(List<Map<String, dynamic>>? relations, String principal) =>`,
5485
+ `bool canView${cap(agg)}(List<RelationTupleReadModel>? relations, String principal) =>`,
5561
5486
  ` _holdsAnyRelation(relations, ${dartSet(rels)}, principal);`,
5562
5487
  );
5563
5488
  }