@githolon/dsl 0.101.5 → 0.102.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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
@@ -59,7 +59,6 @@ import {
59
59
  generateDartActivityRegistry,
60
60
  collectActivityDescriptors,
61
61
  collectPayloadIdOfTargets,
62
- typedIdClassForAggregateType,
63
62
  type DartImport,
64
63
  type DartRefImport,
65
64
  type DomainModule,
@@ -67,14 +66,28 @@ import {
67
66
  } from "./codegen_dart.js";
68
67
  import type { ContractHandle } from "./contract.js";
69
68
  import { generateDartBridgeClient } from "./codegen_dart_client.js";
69
+ import {
70
+ dartApplicationClassName,
71
+ dartApplicationPublicClassNames,
72
+ generateDartApplication,
73
+ } from "./codegen_dart_application.js";
74
+ import {
75
+ dartScenarioGivenClassName,
76
+ dartScenarioClassName,
77
+ dartTestPersonClassName,
78
+ generateDartScenario,
79
+ } from "./codegen_dart_scenario.js";
70
80
  import { buildNomosDescribeModel } from "./describe_model.js";
71
81
  import {
82
+ FRAMEWORK_AGGREGATE_IDS,
83
+ FRAMEWORK_DIRECTIVE_IDS,
72
84
  isFrameworkAggregate,
73
85
  isFrameworkDirective,
74
86
  isFrameworkQuery,
75
87
  } from "./framework/framework_aggregates.js";
76
88
  import { RelationTuple, relationTuplesByObject } from "./framework/relations.js";
77
89
  import { isNamespacedAggregate } from "./read_projection_namespace.js";
90
+ import { collectPayloadValueObjectDescriptors } from "./value_object.js";
78
91
  import { finishCount, type AnyCount } from "./count.js";
79
92
  import type { QueryDecl } from "./query.js";
80
93
  import type { SpatialDecl } from "./spatial.js";
@@ -88,7 +101,6 @@ import {
88
101
  composeDomainModule,
89
102
  directivesOf,
90
103
  emitUsdJsonForModules,
91
- packageUsda,
92
104
  sha256HexUtf8,
93
105
  type Mod,
94
106
  } from "./build_package.js";
@@ -99,6 +111,10 @@ import { expandBusinessBirthExports } from "./business_birth.js";
99
111
  import { withOwnedCustodyExports } from "./framework/owned_custody.js";
100
112
  import { expandRoleExports, rolesOf } from "./role.js";
101
113
  import { collectEngineRouting } from "./engine_entry.js";
114
+ import {
115
+ canonicalizeEngineJavascript,
116
+ canonicalizeGeneratedClientJavascript,
117
+ } from "./engine_javascript.js";
102
118
  import {
103
119
  discover,
104
120
  isCountDecl,
@@ -113,12 +129,21 @@ import { canonicalTaxonomyFragment, validateWorkspaceTaxonomyAndRoutes } from ".
113
129
  import { shardingLawModule, type ShardingLawSpec } from "./workspace_sharding.js";
114
130
  import { finishExtremum, type AnyExtremum } from "./extremum.js";
115
131
  import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
116
- import { type RelationTypeDecl, isRelationTypeDecl, validateRelationType } from "./relation_schema.js";
132
+ import {
133
+ discoverRelationTypeDeclarations,
134
+ mergeRelationTypeDeclarations,
135
+ type RelationTypeDecl,
136
+ } from "./relation_schema.js";
117
137
  import {
118
138
  emitLayeredDomain,
119
139
  flattenLayeredUsd,
120
140
  layeredRootUsda,
141
+ nativePlanPackageUsda,
142
+ parseUsdaDocument,
143
+ usdaLayerText,
121
144
  type LayeredModuleInput,
145
+ type UsdPlanAsset,
146
+ type UsdPlanRoute,
122
147
  } from "./usd_layers.js";
123
148
  import { domainManifest, canonicalJson } from "./manifest.js";
124
149
  import type { WireSchema } from "./wire.js";
@@ -130,7 +155,7 @@ import {
130
155
  import { emitUsd, type UsdLayer } from "./usd.js";
131
156
  import { emitLayeredRelationUsda, type RelationLayerInput } from "./relation_composition.js";
132
157
  import { assertNoDartBarrelCollisions, type BarrelExportedFile } from "./dart_barrel_guard.js";
133
- import { generateTsClient, tsClientFactoryName, recursiveLazyTypes } from "./codegen_ts.js";
158
+ import { generateTsClient, tsApplicationName, tsClientFactoryName, recursiveLazyTypes } from "./codegen_ts.js";
134
159
  import { generateOpenApiFragment, mergeOpenApiFragments } from "./codegen_openapi.js";
135
160
  import { domainInterface, type DomainInterface } from "./shape_compat.js";
136
161
  import { generateTsProof, proofLegsIndex } from "./codegen_proof.js";
@@ -138,6 +163,14 @@ import { checkDomainModules, formatFindings, hasRefusal } from "./law_check.js";
138
163
  import type { NomosCompat } from "./compat_manifest.js";
139
164
  import { requiredKernelFeatures, type FeatureProbeModule } from "./kernel_capabilities.js";
140
165
  import { APPLICATION_HOME_DOMAIN } from "./framework/application_home_domain.js";
166
+ import { resolveApplicationTopology } from "./application_topology.js";
167
+ import { isBusinessIntent } from "./intent.js";
168
+ import {
169
+ compileApplicationComposition,
170
+ isApplicationCompositionConfig,
171
+ packageAssetUsda,
172
+ type PackageRootPart,
173
+ } from "./application_composition.js";
141
174
 
142
175
  const DSL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
143
176
 
@@ -188,6 +221,8 @@ type DartSupportImport = "vendored" | "package";
188
221
  interface PackageConfig {
189
222
  readonly name: string;
190
223
  readonly domains: readonly DomainConfig[];
224
+ /** The one OpenUSD model root this independently published package exports. */
225
+ readonly rootPart?: PackageRootPart;
191
226
  /** reportId → "modulePath#exportName" (the export is `(actor) => Report`). */
192
227
  readonly reports?: Record<string, string>;
193
228
  readonly outDir?: string;
@@ -195,7 +230,7 @@ interface PackageConfig {
195
230
  * Typed DART frontend (the Flutter sibling of the generated TS client; #codegen_dart).
196
231
  * `false`/absent = skip (default). `true` = emit into `<outDir>/dart/`;
197
232
  * `{ out: "relative/dir" }` = emit there (resolved against the config dir).
198
- * `{ support: "package" }` keeps `package:nomos_client/nomos_client.dart` imports for Flutter apps that
233
+ * `{ support: "package" }` keeps the framework-internal runtime import for generated Flutter applications that
199
234
  * already depend on `nomos_client`/`nomos_flutter`. The default `"vendored"` mode preserves the old
200
235
  * self-contained directory.
201
236
  * Per composed domain one `<name>.dart` (payload classes + read models + typed read
@@ -224,7 +259,7 @@ interface PackageConfig {
224
259
  * pure `nomos_types` core — frontend-safe: no bridge, no sessions, no lifecycle.
225
260
  * • `client` — the effectful runtime clients (`<name>.client.dart`, offer methods over
226
261
  * NomosBridge). Depends on the types package + `nomos_client`.
227
- * • `test` — optional; the headless-harness surface for widget/integration tests.
262
+ * • `test` — optional; generated business scenarios for widget/integration tests.
228
263
  * The layering line: UI code depends on `types`; composition/runtime code on `client`;
229
264
  * generated TYPES may flow to the UI, generated CLIENTS may not.
230
265
  * Each package's `pubspec.yaml` is scaffolded ONLY IF MISSING (tenant-owned versioning);
@@ -271,26 +306,8 @@ interface PackageConfig {
271
306
  readonly names?: Readonly<Record<string, string>>;
272
307
  };
273
308
  /**
274
- * OPT-IN layered USD emission (also `--layered` on the CLI). When true, ALSO emit
275
- * `build/layers/<domain>--<module>.usda` (+ `.layer.json`) — one REAL USD layer per
276
- * authored MODULE — and `build/<name>.layered.usda`, a root whose subLayers stack
277
- * them (USD strongest-first == Nomos module order reversed). The flattened
278
- * `.package.usda` emission and the domainHash DO NOT MOVE; the compile asserts
279
- * fail-closed that the layer stack flattens BYTE-IDENTICALLY to the canonical
280
- * composed IR (the monoid homomorphism — see `usd_layers.ts` /
281
- * `docs/usd_layered_composition.md`).
282
- */
283
- readonly layered?: boolean;
284
- /**
285
- * THE COMPACT PACKAGE ERA (opt-in; also `--compact` / NOMOS_COMPACT=1). ONE batched,
286
- * NAMED domainHash move that shrinks the package ~4x (guestbook 1.48MB → ~0.35MB):
287
- * • minified engine lump (esbuild minify — string literals, and so refusal codes, unchanged);
288
- * • zod locales stripped to `en` only (the locale barrel is 35% of every lump and is
289
- * namespace-re-exported so tree-shaking can't drop it; `en` keeps error messages
290
- * byte-identical to today's default);
291
- * • base64 blob fields (`*B64`, 1.33x text) instead of hex (2x).
292
- * REQUIRES the whole consuming stack (cloud kernel + clients) at ≥ the release that shipped
293
- * the decode arms. Default stays off until a future major; hex packages parse forever.
309
+ * Minify each content-addressed execution plan asset. Native USD law remains readable and
310
+ * unchanged; this only changes the implementation asset bytes and therefore the package hash.
294
311
  */
295
312
  readonly compact?: boolean;
296
313
  }
@@ -300,6 +317,67 @@ function fail(msg: string): never {
300
317
  process.exit(1);
301
318
  }
302
319
 
320
+ /** Locate stock OpenUSD's usdcat without making it a runtime dependency. */
321
+ function resolveUsdcat(): string | undefined {
322
+ const candidates = [process.env.NOMOS_USDCAT, "/usr/bin/usdcat", "usdcat"]
323
+ .filter((candidate): candidate is string => typeof candidate === "string" && candidate.length > 0);
324
+ for (const candidate of [...new Set(candidates)]) {
325
+ try {
326
+ execFileSync(candidate, ["--help"], { stdio: "ignore" });
327
+ return candidate;
328
+ } catch {
329
+ // Try the next stock-tool location.
330
+ }
331
+ }
332
+ return undefined;
333
+ }
334
+
335
+ /** Name the first structural disagreement between two canonical law values. */
336
+ function firstLawDifference(expected: unknown, actual: unknown, at = "$law"): string | undefined {
337
+ if (Object.is(expected, actual)) return undefined;
338
+ if (Array.isArray(expected) || Array.isArray(actual)) {
339
+ if (!Array.isArray(expected) || !Array.isArray(actual)) {
340
+ return `${at}: expected ${Array.isArray(expected) ? "an array" : typeof expected}, got ${Array.isArray(actual) ? "an array" : typeof actual}`;
341
+ }
342
+ if (expected.length !== actual.length) {
343
+ const ids = (items: unknown[]): string[] => items.flatMap((item) =>
344
+ item !== null && typeof item === "object" && typeof (item as { id?: unknown }).id === "string"
345
+ ? [(item as { id: string }).id]
346
+ : []);
347
+ const expectedIds = ids(expected);
348
+ const actualIds = ids(actual);
349
+ const missing = expectedIds.filter((id) => !actualIds.includes(id));
350
+ const extra = actualIds.filter((id) => !expectedIds.includes(id));
351
+ return `${at}.length: expected ${expected.length}, got ${actual.length}` +
352
+ `${missing.length > 0 ? `; missing [${missing.join(", ")}]` : ""}` +
353
+ `${extra.length > 0 ? `; extra [${extra.join(", ")}]` : ""}`;
354
+ }
355
+ for (let index = 0; index < expected.length; index++) {
356
+ const difference = firstLawDifference(expected[index], actual[index], `${at}[${index}]`);
357
+ if (difference !== undefined) return difference;
358
+ }
359
+ return undefined;
360
+ }
361
+ if (expected !== null && actual !== null && typeof expected === "object" && typeof actual === "object") {
362
+ const expectedObject = expected as Record<string, unknown>;
363
+ const actualObject = actual as Record<string, unknown>;
364
+ if (typeof expectedObject.path === "string" && expectedObject.path === actualObject.path) {
365
+ at = `${at}{path=${expectedObject.path}}`;
366
+ }
367
+ const expectedKeys = Object.keys(expectedObject).sort();
368
+ const actualKeys = Object.keys(actualObject).sort();
369
+ if (JSON.stringify(expectedKeys) !== JSON.stringify(actualKeys)) {
370
+ return `${at}: expected keys [${expectedKeys.join(", ")}], got [${actualKeys.join(", ")}]`;
371
+ }
372
+ for (const key of expectedKeys) {
373
+ const difference = firstLawDifference(expectedObject[key], actualObject[key], `${at}.${key}`);
374
+ if (difference !== undefined) return difference;
375
+ }
376
+ return undefined;
377
+ }
378
+ return `${at}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`;
379
+ }
380
+
303
381
  // ── decl auto-discovery (by SHAPE, mirroring the engine entry's duck-types) ───────
304
382
  // The query/workspace-invariant/spatial/sum/count predicates + `discover` live in the SHARED
305
383
  // bundle-safe `decl_shapes.ts` (imported above) — the lump's self-describe verb discovers by the
@@ -432,7 +510,7 @@ function resolveNamed<T>(merged: Mod, names: readonly string[] | undefined, what
432
510
  * Vendored output rewrites it to the relative barrel so the directory is
433
511
  * self-contained (no pub path-dependency on the framework package).
434
512
  */
435
- const DART_PACKAGE_IMPORT = `import 'package:nomos_client/nomos_client.dart';`;
513
+ const DART_PACKAGE_IMPORT = `import 'package:nomos_client/src/nms_generated_runtime.dart';`;
436
514
  const DART_RELATIVE_IMPORT = `import 'nomos_client.dart';`;
437
515
 
438
516
  /**
@@ -482,10 +560,50 @@ const DART_SUPPORT_CORE_SRC = [
482
560
  // `package:nomos_types/...` imports are rewritten to the local vendored shim (see emitDart).
483
561
  // (`home.dart` is NOT vendored — the home-onboarding runtime drags `package:crypto`; nothing the
484
562
  // codegen emits references it.)
485
- const DART_SUPPORT_RUNTIME_SRC = ["provider.dart", "webview_bridge.dart", "session_bound.dart"] as const;
563
+ const DART_SUPPORT_RUNTIME_SRC = [
564
+ "provider.dart",
565
+ "webview_bridge.dart",
566
+ "session_bound.dart",
567
+ "record_origin.dart",
568
+ ] as const;
486
569
  const DART_SUPPORT_SRC = [...DART_SUPPORT_CORE_SRC, ...DART_SUPPORT_RUNTIME_SRC] as const;
487
570
  /** The local shim replacing `package:nomos_types/nomos_types.dart` inside vendored runtime files. */
488
571
  const VENDORED_TYPES_SHIM = "nomos_types.dart";
572
+ const VENDORED_META_DEP = "^1.12.0";
573
+
574
+ /**
575
+ * Vendored runtime files carry package:meta's @internal boundary. Keep the
576
+ * containing Dart package honest by declaring that implementation dependency
577
+ * instead of stripping the annotation from generated copies.
578
+ */
579
+ function ensureVendoredMetaDependency(dartOut: string): void {
580
+ let dir = path.resolve(dartOut);
581
+ let pubspec: string | undefined;
582
+ for (let hops = 0; hops < 8; hops++) {
583
+ const candidate = path.join(dir, "pubspec.yaml");
584
+ if (existsSync(candidate)) {
585
+ pubspec = candidate;
586
+ break;
587
+ }
588
+ const parent = path.dirname(dir);
589
+ if (parent === dir) break;
590
+ dir = parent;
591
+ }
592
+ if (pubspec === undefined) return;
593
+ const source = readFileSync(pubspec, "utf8");
594
+ if (/^ meta:\s*/m.test(source)) return;
595
+ if (!/^dependencies:\s*$/m.test(source)) {
596
+ fail("generated Dart vendored support needs a dependencies: block in " + pubspec);
597
+ }
598
+ writeFileSync(
599
+ pubspec,
600
+ source.replace(
601
+ /^dependencies:\s*$/m,
602
+ "dependencies:\n meta: " + VENDORED_META_DEP,
603
+ ),
604
+ "utf8",
605
+ );
606
+ }
489
607
 
490
608
  /** The generated-owned subdirectory, relative to `dartOut` (the pub package's `lib/`). WIPED and
491
609
  * rewritten on every compile — nothing hand-written may live in it. The stable facade (the package
@@ -510,14 +628,6 @@ function collectPackageContracts(modules: readonly DomainModule[]): ContractHand
510
628
  return [...byId.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
511
629
  }
512
630
 
513
- /** Every aggregate's generated typed-id class name across the package — the collision guard for the
514
- * contract typed-id emission (a contract named after a local aggregate must not re-declare `<X>Id`). */
515
- function packageAggregateIdClassNames(modules: readonly DomainModule[]): Set<string> {
516
- const out = new Set<string>();
517
- for (const m of modules) for (const a of m.aggregates) out.add(typedIdClassForAggregateType(a.id));
518
- return out;
519
- }
520
-
521
631
  /**
522
632
  * CROSS-DOMAIN REF TRAVERSAL map (dart_codegen_parity.md §2a follow-on): every aggregate id in the package
523
633
  * → its owning domain (`domain` for the generated client class name; `name` for the sibling
@@ -527,11 +637,19 @@ function packageAggregateIdClassNames(modules: readonly DomainModule[]): Set<str
527
637
  */
528
638
  function buildPackageAggregateDomains(
529
639
  modules: readonly DomainModule[],
530
- ): Record<string, { domain: string; name: string }> {
531
- const map: Record<string, { domain: string; name: string }> = {};
640
+ ): Record<string, { domain: string; name: string; namespaced: boolean }> {
641
+ const map: Record<
642
+ string,
643
+ { domain: string; name: string; namespaced: boolean }
644
+ > = {};
532
645
  for (const m of modules) {
533
- const owner = { domain: m.domain ?? m.name, name: m.name };
534
- for (const agg of (m.aggregates as { id: string }[])) map[agg.id] = owner;
646
+ for (const agg of m.aggregates) {
647
+ map[agg.id] = {
648
+ domain: m.domain ?? m.name,
649
+ name: m.name,
650
+ namespaced: isNamespacedAggregate(agg),
651
+ };
652
+ }
535
653
  }
536
654
  return map;
537
655
  }
@@ -565,7 +683,7 @@ function guardNoConcreteImportCycle(
565
683
  fail(
566
684
  `domain contracts: concrete cross-domain import cycle between '${from}' and '${to}' — their ` +
567
685
  `generated Dart files would import each other (a t.foreignRef in each targets a concrete aggregate ` +
568
- `owned by the other). Break it with a domain CONTRACT: declare a contract(...) for the shared shape, ` +
686
+ `owned by the other). Break it with a record contract: declare recordContract(...) for the shared shape, ` +
569
687
  `.implements(...) it on the owning aggregate, and point the foreignRef at the CONTRACT (it resolves ` +
570
688
  `through the shared contracts.dart, keeping the domain graph acyclic).`,
571
689
  );
@@ -574,12 +692,77 @@ function guardNoConcreteImportCycle(
574
692
  }
575
693
  }
576
694
 
695
+ /**
696
+ * Close the duplicate direct-birth presentation lane after the whole package has been composed.
697
+ *
698
+ * A `birth(A)` declaration is reusable law. When another domain publishes
699
+ * `intent(...).births(thatDeclaration)`, however, the wider intent is the ONE tenant action: exposing
700
+ * the declaration's direct `nomosBirthA` route as well would let callers skip the intent's other
701
+ * consequences. `business_birth.ts` can suppress that route by object identity when module evaluation
702
+ * happens in the convenient order, but package domains are deliberately lazy and their order must not
703
+ * change the public API. Resolve the ownership semantically across the completed package instead.
704
+ *
705
+ * This is presentation-only. The direct directive remains installed law for genesis/replay; generated
706
+ * clients, public OpenAPI and proofs see it as an implementation detail. Authority is unaffected.
707
+ */
708
+ function closeComposedBusinessBirthPresentation(
709
+ modules: readonly DomainModule[],
710
+ ): DomainModule[] {
711
+ type BirthMetadata = {
712
+ aggregateId?: unknown;
713
+ rootId?: unknown;
714
+ namespace?: unknown;
715
+ keyField?: unknown;
716
+ };
717
+ type BirthDirective = DomainModule["directives"][number] & {
718
+ businessBirth?: BirthMetadata;
719
+ businessBirthInternal?: true;
720
+ };
721
+ const keyOf = (directive: BirthDirective): string | undefined => {
722
+ const birth = directive.businessBirth;
723
+ if (
724
+ birth === undefined ||
725
+ typeof birth.aggregateId !== "string" ||
726
+ typeof birth.rootId !== "string" ||
727
+ typeof birth.namespace !== "string" ||
728
+ typeof birth.keyField !== "string"
729
+ ) return undefined;
730
+ return [birth.aggregateId, birth.rootId, birth.namespace, birth.keyField].join("\u0000");
731
+ };
732
+
733
+ const composedBirths = new Set<string>();
734
+ for (const mod of modules) {
735
+ for (const directive of mod.directives as BirthDirective[]) {
736
+ if (!isBusinessIntent(directive)) continue;
737
+ const key = keyOf(directive);
738
+ if (key !== undefined) composedBirths.add(key);
739
+ }
740
+ }
741
+ if (composedBirths.size === 0) return [...modules];
742
+
743
+ const hideDirectBirth = (directive: BirthDirective): BirthDirective => {
744
+ if (isBusinessIntent(directive)) return directive;
745
+ const key = keyOf(directive);
746
+ return key !== undefined && composedBirths.has(key)
747
+ ? { ...directive, businessBirthInternal: true }
748
+ : directive;
749
+ };
750
+
751
+ return modules.map((mod) => ({
752
+ ...mod,
753
+ directives: (mod.directives as BirthDirective[]).map(hideDirectBirth),
754
+ ...(mod.applicationIntents === undefined
755
+ ? {}
756
+ : { applicationIntents: (mod.applicationIntents as BirthDirective[]).map(hideDirectBirth) }),
757
+ }));
758
+ }
759
+
577
760
  /** FRAMEWORK-SHARED symbol dedup, shared by the flat emit and the layered-packages emit (Dart-codegen
578
761
  * hygiene): framework-owned symbols fold into MANY domains, so their generated Dart would be DEFINED
579
762
  * once per domain — a package barrel re-exporting ≥2 domains then COLLIDES. THREE kinds are deduped,
580
763
  * each identified by the framework BRAND (object identity, so a tenant symbol that merely shares a
581
764
  * name is never misclassified) and emitted EXACTLY ONCE into `nomos_framework.dart`:
582
- * • AGGREGATES — read-model class, typed-id class, by-id accessors;
765
+ * • AGGREGATES — projection/read-model class and by-id accessors;
583
766
  * • DIRECTIVES — the generated payload class (the domain still OFFERS them per-domain);
584
767
  * • QUERIES — the top-level `watch<Q>`/`read<Q>` accessors.
585
768
  * Plus VALUE OBJECTS used by ≥2 domains (same collision rule). Per-domain files SKIP each and
@@ -593,6 +776,40 @@ interface DartSharedSymbols {
593
776
  /** The synthetic module whose emit IS `nomos_framework.dart` — undefined when nothing is shared. */
594
777
  readonly frameworkMod: DomainModule | undefined;
595
778
  }
779
+
780
+ /** Every named value object whose Dart type this domain needs, including payload-only VOs pulled
781
+ * across a domain boundary by a composed business birth. `mod.valueObjects` only sees aggregate
782
+ * fields; without the payload half a parent `births(childBirth)` re-emits the child's VO locally and
783
+ * the flat package barrel becomes ambiguous. Count once per domain, not once per use. */
784
+ function dartValueObjectsForModule(
785
+ mod: DomainModule,
786
+ ): NonNullable<DomainModule["valueObjects"]> {
787
+ const codegenDirectives = (mod.applicationIntents ?? mod.directives)
788
+ .filter((d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true)
789
+ .map((d) => {
790
+ const birth = (d as {
791
+ businessBirth?: { publicPayloadSchema?: unknown };
792
+ }).businessBirth;
793
+ return birth?.publicPayloadSchema === undefined
794
+ ? d
795
+ : { ...d, payloadSchema: birth.publicPayloadSchema };
796
+ });
797
+ const byName = new Map<string, NonNullable<DomainModule["valueObjects"]>[number]>();
798
+ for (const descriptor of [
799
+ ...(mod.valueObjects ?? []),
800
+ ...collectPayloadValueObjectDescriptors(codegenDirectives),
801
+ ]) {
802
+ const prior = byName.get(descriptor.name);
803
+ if (prior !== undefined && JSON.stringify(prior) !== JSON.stringify(descriptor)) {
804
+ throw new Error(
805
+ `value object '${descriptor.name}' has DIVERGENT aggregate/payload shapes in domain '${mod.domain ?? mod.name}'`,
806
+ );
807
+ }
808
+ byName.set(descriptor.name, descriptor);
809
+ }
810
+ return [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
811
+ }
812
+
596
813
  function collectDartSharedSymbols(
597
814
  modules: readonly DomainModule[],
598
815
  packageContracts: readonly ContractHandle[] = [],
@@ -616,13 +833,13 @@ function collectDartSharedSymbols(
616
833
  const voDomainsByName = new Map<string, number>();
617
834
  const voDescriptorByName = new Map<string, NonNullable<DomainModule["valueObjects"]>[number]>();
618
835
  for (const mod of modules) {
619
- for (const d of mod.valueObjects ?? []) {
836
+ for (const d of dartValueObjectsForModule(mod)) {
620
837
  voDomainsByName.set(d.name, (voDomainsByName.get(d.name) ?? 0) + 1);
621
838
  voDescriptorByName.set(d.name, d);
622
839
  }
623
840
  }
624
841
  // CONTRACT-USED VOs ARE LAYER-ZERO (co2 #117): a VO referenced by a contract FIELD (e.g. a
625
- // `ReferenceCode.field()` inside `contract(...)`) is rendered inside `contracts.dart` — the shared layer
842
+ // `ReferenceCode.field()` inside `recordContract(...)`) is rendered inside `contracts.dart` — the shared layer
626
843
  // every domain imports and which may import NO domain file (the acyclic rule). So its class must live in
627
844
  // `nomos_framework.dart` REGARDLESS of how many domains use it (a single-domain VO would otherwise land in
628
845
  // that domain's file, unreachable from contracts.dart). Transitive deps included (a nested VO renders too).
@@ -694,7 +911,7 @@ function domainDedupOpts(mod: DomainModule, shared: DartSharedSymbols): {
694
911
  (modForDart.queries ?? []).filter((q) => shared.frameworkQueryIds.has(q.id)).map((q) => q.id),
695
912
  );
696
913
  const skipValueObjectNames = new Set(
697
- (modForDart.valueObjects ?? []).filter((d) => shared.sharedVoNames.has(d.name)).map((d) => d.name),
914
+ dartValueObjectsForModule(modForDart).filter((d) => shared.sharedVoNames.has(d.name)).map((d) => d.name),
698
915
  );
699
916
  const foldsFramework =
700
917
  skipAggregateIds.size > 0 || skipDirectiveIds.size > 0 || skipQueryIds.size > 0 || skipValueObjectNames.size > 0;
@@ -799,6 +1016,7 @@ function emitDart(
799
1016
  // 1. optional vendored framework support (barrel + src/), copied byte-for-byte except
800
1017
  // the barrel's schema_validation export (see DART_SUPPORT_SRC).
801
1018
  if (supportImport === "vendored") {
1019
+ ensureVendoredMetaDependency(dartOut);
802
1020
  mkdirSync(path.join(genDir, "src"), { recursive: true });
803
1021
  // Core files come from the pure `nomos_types` package, runtime files from `nomos_client`
804
1022
  // (the layered split). Runtime files import the core via `package:nomos_types/...` — the
@@ -807,10 +1025,15 @@ function emitDart(
807
1025
  const clientDir = path.join(DSL_DIR, "dart", "lib", "src");
808
1026
  for (const f of DART_SUPPORT_CORE_SRC) copyFileSync(path.join(coreDir, f), path.join(genDir, "src", f));
809
1027
  for (const f of DART_SUPPORT_RUNTIME_SRC) {
810
- const src = readFileSync(path.join(clientDir, f), "utf8").replace(
811
- /import 'package:nomos_types\/nomos_types\.dart'/g,
812
- `import '${VENDORED_TYPES_SHIM}'`,
813
- );
1028
+ const src = readFileSync(path.join(clientDir, f), "utf8")
1029
+ .replace(
1030
+ /import 'package:nomos_types\/nomos_types\.dart'/g,
1031
+ `import '${VENDORED_TYPES_SHIM}'`,
1032
+ )
1033
+ .replace(
1034
+ /import 'package:nomos_types\/src\/(?:nomos_ref|foreign_ref)\.dart'/g,
1035
+ `import '${VENDORED_TYPES_SHIM}'`,
1036
+ );
814
1037
  writeFileSync(path.join(genDir, "src", f), src, "utf8");
815
1038
  }
816
1039
  // The local shim the rewritten runtime imports resolve through (the vendored twin of the
@@ -848,7 +1071,7 @@ function emitDart(
848
1071
  // readSignerDelegationByUser, …` band-aid). THREE kinds are deduped, each identified by the
849
1072
  // framework BRAND (object identity, so a tenant symbol that merely shares a name is never
850
1073
  // misclassified) and emitted EXACTLY ONCE into `nomos_framework.dart`:
851
- // • AGGREGATES — read-model class, typed-id class, by-id accessors (`RelationTuple` auto-added
1074
+ // • AGGREGATES — projection/read-model class and by-id accessors (`RelationTuple` auto-added
852
1075
  // to every relation-using domain, `SignerDelegation`/`KeyScope`/… re-exported verbatim);
853
1076
  // • DIRECTIVES — the generated payload class (relate/signer/enroll family). The domain still
854
1077
  // OFFERS them (client offer methods stay per-domain); only the payload TYPE moves to the shared file;
@@ -891,7 +1114,6 @@ function emitDart(
891
1114
  if (packageContracts.length > 0) {
892
1115
  writeGenerated(CONTRACTS_FILE, generateDartContracts(packageContracts, {
893
1116
  ...(frameworkImportUri !== undefined ? { frameworkImportUri } : {}),
894
- aggregateIdClassNames: packageAggregateIdClassNames(modules),
895
1117
  }));
896
1118
  }
897
1119
  // 2a-ter. the shared ACCOUNTABLE ACTIVITY + DESCRIPTION REGISTRY (`activity_registry.dart`) — emitted
@@ -951,7 +1173,7 @@ function emitDart(
951
1173
  }
952
1174
  }
953
1175
  // AUTO cross-domain `idOf(Agg)` imports (dart_codegen_parity.md P2): a payload field typed with a
954
- // SIBLING domain's typed id class (`ListingId`) needs that domain's generated file, exactly as a
1176
+ // SIBLING aggregate's generated type needs that domain's generated file, exactly as a
955
1177
  // foreignRef target does. Same-domain idOf targets declare their id class locally — skip them.
956
1178
  for (const target of collectPayloadIdOfTargets(modForDart.directives)) {
957
1179
  const targetFile = aggIdToDartFile.get(target);
@@ -967,6 +1189,7 @@ function emitDart(
967
1189
  const modForDartWithImports = mergedDartImports.length > 0 ? { ...modForDart, dartImports: mergedDartImports } : modForDart;
968
1190
  const src = generateDartDomain(modForDartWithImports, {
969
1191
  ...skips,
1192
+ packageAggregateDomains,
970
1193
  ...(domainFrameworkImport !== undefined ? { frameworkImportUri: domainFrameworkImport } : {}),
971
1194
  });
972
1195
  writeGenerated(mod.name, src);
@@ -980,11 +1203,25 @@ function emitDart(
980
1203
  emitActivity: true,
981
1204
  activityImportUri,
982
1205
  describeModel,
1206
+ });
1207
+ const applicationSrc = generateDartApplication(modForDart, {
1208
+ supportImport,
983
1209
  packageAggregateDomains,
984
1210
  });
985
- const cp = path.join(genDir, `${mod.name}.client.dart`);
986
- writeFileSync(cp, clientSrc, "utf8");
1211
+ const clientFile = `${mod.name}.client.dart`;
1212
+ const applicationFile = `${mod.name}.application.dart`;
1213
+ const encapsulated = encapsulateDartClient(
1214
+ clientSrc,
1215
+ applicationSrc,
1216
+ clientFile,
1217
+ applicationFile,
1218
+ );
1219
+ const cp = path.join(genDir, clientFile);
1220
+ writeFileSync(cp, encapsulated.clientPart, "utf8");
987
1221
  written.push(cp);
1222
+ const ap = path.join(genDir, `${mod.name}.application.dart`);
1223
+ writeFileSync(ap, encapsulated.applicationLibrary, "utf8");
1224
+ written.push(ap);
988
1225
  }
989
1226
 
990
1227
  // 3. THE PACKAGE BARREL — the stable public facade (`dartOut/<barrelName>.dart`), generated-owned
@@ -1024,15 +1261,24 @@ function emitDart(
1024
1261
  .map((f) => `${genRel}/${f}.dart`);
1025
1262
  })(),
1026
1263
  ),
1264
+ // The generated clients import their runtime support directly. Keeping it out of the public barrel is
1265
+ // the surface firewall: a tenant import receives business types and application APIs, never raw runtime.
1027
1266
  supportImport === "vendored"
1028
- ? `export '${genRel}/nomos_client.dart';`
1029
- : `export 'package:nomos_client/nomos_client.dart';`,
1030
- ...(hasFrameworkSymbols ? [`export '${genRel}/${FRAMEWORK_FILE}.dart';`] : []),
1031
- ...(contractsImportUri !== undefined ? [`export '${genRel}/${CONTRACTS_FILE}.dart';`] : []),
1032
- ...(activityImportUri !== undefined ? [`export '${genRel}/${ACTIVITY_FILE}.dart';`] : []),
1267
+ ? `export '${genRel}/src/${VENDORED_TYPES_SHIM}' show NomosRef;`
1268
+ : `export 'package:nomos_types/nomos_types.dart' show NomosRef;`,
1269
+ ...(contractsImportUri !== undefined
1270
+ ? tenantDartExport(`${genRel}/${CONTRACTS_FILE}.dart`, sourceByFile.get(CONTRACTS_FILE) ?? "")
1271
+ : []),
1272
+ ...(activityImportUri !== undefined
1273
+ ? tenantDartExport(
1274
+ `${genRel}/${ACTIVITY_FILE}.dart`,
1275
+ sourceByFile.get(ACTIVITY_FILE) ?? "",
1276
+ ["renderActivity", "activityNarrative"],
1277
+ )
1278
+ : []),
1033
1279
  ...modules.flatMap((m) => [
1034
- `export '${genRel}/${m.name}.dart';`,
1035
- `export '${genRel}/${m.name}.client.dart';`,
1280
+ ...tenantDartExport(`${genRel}/${m.name}.dart`, sourceByFile.get(m.name) ?? ""),
1281
+ `export '${genRel}/${m.name}.application.dart' show ${dartApplicationPublicClassNames(m).join(", ")};`,
1036
1282
  ]),
1037
1283
  ...dartAliasLines(
1038
1284
  names,
@@ -1052,17 +1298,23 @@ function emitDart(
1052
1298
  // ── the LAYERED-PACKAGES Dart emit (dart.packages: blast-radius isolation) ────────
1053
1299
 
1054
1300
  /** The pure-core support import generated TYPES files carry in layered-packages mode. */
1055
- const DART_TYPES_IMPORT = `import 'package:nomos_types/nomos_types.dart';`;
1301
+ const DART_TYPES_IMPORT = [
1302
+ `import 'package:nomos_types/nomos_types.dart';`,
1303
+ `import 'package:nomos_types/src/nomos_ref.dart' show NmsInternalNomosRefs;`,
1304
+ `import 'package:nomos_types/src/foreign_ref.dart' show NmsInternalForeignRefs;`,
1305
+ `import 'package:nomos_types/src/invitation.dart' show NmsInternalInvitations, NmsInternalRecipients;`,
1306
+ ].join("\n");
1056
1307
  /** Version constraints the scaffolded pubspecs pin — move together with the Dart packages' releases.
1057
1308
  * Caret on 0.x is EXACT-MINOR (`^0.45.0` = `<0.46.0`), so these must track the CURRENT release, not
1058
1309
  * just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
1059
1310
  // 0.3.0: SurfacedValue<T>/SurfacedObservation/PendingConflictRow — generated Surfaced-field typing +
1060
1311
  // the pendingConflicts read rows.
1061
- const NOMOS_TYPES_DEP = "^0.7.0";
1312
+ const NOMOS_TYPES_DEP = "^0.8.0";
1062
1313
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1063
1314
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1064
1315
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1065
- const NOMOS_CLIENT_DEP = "^0.63.1";
1316
+ const NOMOS_CLIENT_DEP = "^0.64.0";
1317
+ const NOMOS_FLUTTER_DEP = "^0.63.0";
1066
1318
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1067
1319
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1068
1320
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -1078,13 +1330,327 @@ const aliasImportLines = (names: Readonly<Record<string, string>> | undefined, u
1078
1330
  * The alias validator and the cursed-name report both read this surface. */
1079
1331
  function declaredDartTypeNames(src: string): Set<string> {
1080
1332
  const names = new Set<string>();
1081
- const re = /^(?:abstract\s+|final\s+|sealed\s+)*(?:class|enum|mixin|extension type)\s+([A-Za-z][A-Za-z0-9_]*)/gm;
1333
+ const re = /^(?:abstract\s+|base\s+|final\s+|sealed\s+|interface\s+|mixin\s+)*(?:class|enum|mixin|extension type)\s+(?:const\s+)?([A-Za-z][A-Za-z0-9_]*)/gm;
1082
1334
  for (let m = re.exec(src); m !== null; m = re.exec(src)) {
1083
1335
  if (!m[1]!.startsWith("_")) names.add(m[1]!);
1084
1336
  }
1085
1337
  return names;
1086
1338
  }
1087
1339
 
1340
+ /** Public declarations safe for the generated application barrel. Runtime/framework names stay in the
1341
+ * implementation packages even when a generated source file imports or declares them. */
1342
+ function tenantDartTypeNames(src: string): string[] {
1343
+ return [...declaredDartTypeNames(src)]
1344
+ // Framework-owned source files are excluded at the file boundary. Do not blanket-hide the `Nomos…`
1345
+ // product vocabulary generated into business files: describe/activity types such as NomosModel and
1346
+ // NomosDescription are required by the generated application. Only the parallel workspace-less ID
1347
+ // wrappers are forbidden here.
1348
+ .sort();
1349
+ }
1350
+
1351
+ /** Named Dart extensions are part of the tenant API too. A `show` barrel that exports the
1352
+ * read-model class but omits its generated conflict extension makes documented getters disappear
1353
+ * at the canonical package import, forcing tenants to deep-import generated files. */
1354
+ function tenantDartExtensionNames(src: string): string[] {
1355
+ const names = new Set<string>();
1356
+ const re = /^extension\s+([A-Za-z][A-Za-z0-9_]*)\s+(?:<[^\n>]+>\s+)?on\s+/gm;
1357
+ for (let match = re.exec(src); match !== null; match = re.exec(src)) {
1358
+ if (!match[1]!.startsWith("_")) names.add(match[1]!);
1359
+ }
1360
+ return [...names].sort();
1361
+ }
1362
+
1363
+ function tenantDartExportNames(src: string): string[] {
1364
+ return [...new Set([...tenantDartTypeNames(src), ...tenantDartExtensionNames(src)])].sort();
1365
+ }
1366
+
1367
+ /**
1368
+ * Turn the generated bridge client into a Dart `part` of the business application library.
1369
+ *
1370
+ * `lib/src` is only a convention: a tenant can deep-import a normal Dart library. A part file is a
1371
+ * language boundary — it cannot be imported on its own — and its underscore-prefixed declarations are
1372
+ * private to the generated application library. This makes the raw bind/open client mechanically
1373
+ * unavailable while leaving the business application as the sole exported entry point.
1374
+ */
1375
+ function encapsulateDartClient(
1376
+ clientSrc: string,
1377
+ applicationSrc: string,
1378
+ clientFile: string,
1379
+ applicationFile: string,
1380
+ ): { readonly clientPart: string; readonly applicationLibrary: string } {
1381
+ const directive = /^(import|export)\s+'([^']+)'(?:\s+[^;]+)?;\s*$/;
1382
+ const clientImportUri = clientFile.replace(/\.dart$/, "");
1383
+ const split = (src: string) => {
1384
+ const directives: string[] = [];
1385
+ const body: string[] = [];
1386
+ const ignores: string[] = [];
1387
+ for (const line of src.split("\n")) {
1388
+ if (line.startsWith("// ignore_for_file:")) {
1389
+ ignores.push(line);
1390
+ } else if (directive.test(line)) {
1391
+ directives.push(line);
1392
+ } else {
1393
+ body.push(line);
1394
+ }
1395
+ }
1396
+ return { directives, body, ignores };
1397
+ };
1398
+ const client = split(clientSrc);
1399
+ const application = split(applicationSrc);
1400
+
1401
+ // Every implementation-side declared type and named extension becomes genuinely library-private.
1402
+ const implementationNames = new Set(declaredDartTypeNames(clientSrc));
1403
+ for (const match of clientSrc.matchAll(/^extension\s+([A-Za-z][A-Za-z0-9_]*)\s+/gm)) {
1404
+ if (match[1] !== undefined && !match[1].startsWith("_")) implementationNames.add(match[1]);
1405
+ }
1406
+ for (const match of clientSrc.matchAll(/^const\s+(?:[A-Za-z][A-Za-z0-9_<>, ?]*\s+)?([a-z][A-Za-z0-9_]*)\s*=/gm)) {
1407
+ if (match[1] !== undefined && !match[1].startsWith("_")) implementationNames.add(match[1]);
1408
+ }
1409
+ const privateNames = new Map(
1410
+ [...implementationNames].map((name) => [name, `_Nms${name}`] as const),
1411
+ );
1412
+ const applicationNames = new Set(declaredDartTypeNames(applicationSrc));
1413
+ const privatize = (src: string, preserve: ReadonlySet<string> = new Set()): string => {
1414
+ let out = src;
1415
+ for (const [name, privateName] of privateNames) {
1416
+ if (preserve.has(name)) continue;
1417
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1418
+ out = out.replace(new RegExp(`\\b${escaped}\\b`, "g"), privateName);
1419
+ }
1420
+ return out;
1421
+ };
1422
+
1423
+ // Merge imports/exports into the application library. Prefer an unrestricted import over a `show`
1424
+ // import of the same URI because the client implementation necessarily uses the fuller surface.
1425
+ const imports = new Map<string, string>();
1426
+ const exports = new Map<string, string>();
1427
+ for (const line of [...client.directives, ...application.directives]) {
1428
+ const match = directive.exec(line);
1429
+ if (match === null) continue;
1430
+ const [, kind, uri] = match;
1431
+ if (kind === "import" && (uri === clientFile || uri === clientImportUri)) continue;
1432
+ // Preserve business-type re-exports so one application import carries its
1433
+ // payloads and read models. Framework/runtime exports belonged to the retired
1434
+ // bridge library and must not enter tenant autocomplete.
1435
+ if (
1436
+ kind === "export" &&
1437
+ client.directives.includes(line) &&
1438
+ /(?:^|\/)(?:nomos_framework|nomos_client)\.dart$/.test(uri!)
1439
+ ) continue;
1440
+ const target = kind === "import" ? imports : exports;
1441
+ const prior = target.get(uri!);
1442
+ if (prior === undefined || (/\sshow\s/.test(prior) && !/\sshow\s/.test(line))) {
1443
+ target.set(uri!, line);
1444
+ }
1445
+ }
1446
+
1447
+ return {
1448
+ clientPart: [
1449
+ "// AUTO-GENERATED by nomos-compile — private implementation part; importing this file is invalid.",
1450
+ ...client.ignores,
1451
+ "// ignore_for_file: unused_element, unused_element_parameter",
1452
+ `part of '${applicationFile}';`,
1453
+ "",
1454
+ privatize(client.body.join("\n")).trimStart(),
1455
+ "",
1456
+ ].join("\n"),
1457
+ applicationLibrary: [
1458
+ "// AUTO-GENERATED by nomos-compile — the sole importable application library.",
1459
+ "// ignore_for_file: unused_import, unused_field, invalid_use_of_internal_member",
1460
+ ...imports.values(),
1461
+ ...exports.values(),
1462
+ `part '${clientFile}';`,
1463
+ "",
1464
+ privatize(application.body.join("\n"), applicationNames).trimStart(),
1465
+ "",
1466
+ ].join("\n"),
1467
+ };
1468
+ }
1469
+
1470
+ const dartApplicationMember = (name: string): string => {
1471
+ const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
1472
+ const joined = words.length === 0
1473
+ ? "domain"
1474
+ : words[0]!.toLowerCase() + words.slice(1).map((word) => word[0]!.toUpperCase() + word.slice(1)).join("");
1475
+ return /^(?:assert|break|case|catch|class|const|continue|default|do|else|enum|extends|false|final|for|if|in|is|new|null|rethrow|return|super|switch|this|throw|true|try|var|void|while|with|dynamic)$/.test(joined)
1476
+ ? `${joined}_`
1477
+ : joined;
1478
+ };
1479
+
1480
+ function generatedPackageApplication(
1481
+ packageName: string,
1482
+ domains: readonly {
1483
+ readonly domain: string;
1484
+ readonly file: string;
1485
+ readonly invitationPolicies: readonly {
1486
+ readonly domainHash: string;
1487
+ readonly query: string;
1488
+ readonly redeem: string;
1489
+ readonly personField: string;
1490
+ readonly roleField: string;
1491
+ readonly scopeField: string;
1492
+ }[];
1493
+ }[],
1494
+ homeDomain: string,
1495
+ rootDomains: readonly string[],
1496
+ ): string {
1497
+ const application = dartApplicationClassName(packageName);
1498
+ const root = domains.find(({ domain }) => domain === homeDomain);
1499
+ if (root === undefined) {
1500
+ throw new Error(`generated Dart package application cannot find its home domain '${homeDomain}'`);
1501
+ }
1502
+ const members = domains
1503
+ .filter(({ domain }) => rootDomains.includes(domain))
1504
+ .map(({ domain, file, invitationPolicies }, index) => ({
1505
+ domain,
1506
+ file,
1507
+ prefix: `_nmsDomain${index}`,
1508
+ member: dartApplicationMember(domain),
1509
+ application: dartApplicationClassName(domain),
1510
+ invitationPolicies,
1511
+ }));
1512
+ if (members.length !== rootDomains.length) {
1513
+ throw new Error("generated Dart package application could not bind every co-resident root domain");
1514
+ }
1515
+ const invitationPolicies = members.flatMap(({ domain, invitationPolicies }) =>
1516
+ invitationPolicies.map((policy) => ({ domain, ...policy })));
1517
+ const hasInvitations = invitationPolicies.length > 0;
1518
+ const duplicate = members.find((candidate, index) =>
1519
+ members.findIndex((other) => other.member === candidate.member) !== index);
1520
+ if (duplicate !== undefined) {
1521
+ throw new Error(
1522
+ `generated Dart package application member '${duplicate.member}' is ambiguous; rename one colliding domain`,
1523
+ );
1524
+ }
1525
+ const frameworkMembers = new Map<string, string>([
1526
+ ["nomosWidget", "the generated Nomos Flutter bootstrap"],
1527
+ ["currentUser", "the generated signed-in application participant"],
1528
+ ["currentStatus", "the generated application status"],
1529
+ ["watchStatus", "the generated application status watch"],
1530
+ ["recipient", "the generated typed recipient boundary"],
1531
+ ["createInvitation", "the generated invitation capability"],
1532
+ ["redeemInvitation", "the generated invitation redemption"],
1533
+ ]);
1534
+ const frameworkCollision = members.find(({ member }) => frameworkMembers.has(member));
1535
+ if (frameworkCollision !== undefined) {
1536
+ throw new Error(
1537
+ `Sorry mate — domain '${frameworkCollision.domain}' claims generated Dart application member ` +
1538
+ `'${frameworkCollision.member}', which is already owned by ${frameworkMembers.get(frameworkCollision.member)}. ` +
1539
+ `Nomos will not silently rename or reroute it; rename the tenant domain and compile again.`,
1540
+ );
1541
+ }
1542
+ return [
1543
+ `// AUTO-GENERATED by nomos-compile — the one tenant application for "${packageName}".`,
1544
+ `// Domain clients remain generated implementation details behind this stateful business surface.`,
1545
+ `// ignore_for_file: invalid_use_of_internal_member`,
1546
+ `import 'package:flutter/widgets.dart';`,
1547
+ `import 'package:nomos_flutter/src/application_status.dart' show ApplicationSignIn, ApplicationStatus;`,
1548
+ `import 'package:nomos_client/src/nms_generated_runtime.dart' show NmsInternalUsers, NomosSession, User${hasInvitations ? ", Invitation, Recipient" : ""};`,
1549
+ `import 'package:nomos_flutter/src/generated_application_host.dart' show NmsInternalApplicationHost, NmsInternalApplicationUserBinding${hasInvitations ? ", NmsInternalInvitationPolicy" : ""};`,
1550
+ ...members.map(({ file, prefix }) =>
1551
+ `import '${file}.application.dart' as ${prefix};`),
1552
+ ``,
1553
+ `/// The one canonical stateful application for ${packageName}.`,
1554
+ `///`,
1555
+ `/// Keep business UI and services on this object. Nomos owns identity, persistence,`,
1556
+ `/// synchronization, workspace navigation, access delivery and recovery underneath it;`,
1557
+ `/// do not build a second runtime host, repository, session, or application-state mirror.`,
1558
+ `///`,
1559
+ `/// Canonical bootstrap:`,
1560
+ "/// ```dart",
1561
+ `/// return ${application}.nomosWidget(`,
1562
+ `/// cloud: cloud,`,
1563
+ `/// application: '${packageName}',`,
1564
+ `/// signIn: signIn,`,
1565
+ `/// builder: (context, application) => MyBusinessUi(application: application),`,
1566
+ `/// );`,
1567
+ "/// ```",
1568
+ `/// Pass that same [${application}] down. It is the application state.`,
1569
+ `final class ${application} {`,
1570
+ ` final NomosSession _nmsSession;`,
1571
+ ...(hasInvitations ? [` final NmsInternalApplicationUserBinding _nmsUser;`] : []),
1572
+ ` final User currentUser;`,
1573
+ ...members.flatMap(({ domain, member, application: child, prefix }) => [
1574
+ ` /// Business actions, records and watches declared by ${domain}.`,
1575
+ ` final ${prefix}.${child} ${member};`,
1576
+ ]),
1577
+ ` const ${application}._({required NomosSession nmsSession, ${hasInvitations ? "required NmsInternalApplicationUserBinding nmsUser, " : ""}required this.currentUser, ${members.map(({ member }) => `required this.${member}`).join(", ")}}) : _nmsSession = nmsSession${hasInvitations ? ", _nmsUser = nmsUser" : ""};`,
1578
+ ``,
1579
+ ` /// Current durability and convergence across this business application.`,
1580
+ ` ApplicationStatus get currentStatus => NmsInternalApplicationHost.currentStatus(_nmsSession);`,
1581
+ ``,
1582
+ ` /// React whenever this application saves, receives work, goes offline or needs attention.`,
1583
+ ` Stream<ApplicationStatus> watchStatus() => NmsInternalApplicationHost.watchStatus(_nmsSession);`,
1584
+ ...(hasInvitations
1585
+ ? [
1586
+ ``,
1587
+ ` /// Turn an identity-provider uid into a typed invitation recipient.`,
1588
+ ` Recipient<User> recipient(String uid) => NmsInternalApplicationHost.recipient<User>(uid);`,
1589
+ ``,
1590
+ ` /// Create an invitation; transport its link however your product chooses.`,
1591
+ ` Future<Invitation<User>> createInvitation() => NmsInternalApplicationHost.createInvitation<User>(_nmsSession);`,
1592
+ ``,
1593
+ ` /// Redeem a transported invitation as [currentUser].`,
1594
+ ` Future<void> redeemInvitation(String link) => NmsInternalApplicationHost.redeemInvitation<User>(`,
1595
+ ` _nmsSession,`,
1596
+ ` link: link,`,
1597
+ ` identity: _nmsUser.nmsInternalActor,`,
1598
+ ` policies: [`,
1599
+ ...invitationPolicies.flatMap((policy) => [
1600
+ ` NmsInternalInvitationPolicy(`,
1601
+ ` domain: ${JSON.stringify(policy.domain)},`,
1602
+ ` domainHash: ${JSON.stringify(policy.domainHash)},`,
1603
+ ` query: ${JSON.stringify(policy.query)},`,
1604
+ ` redeem: ${JSON.stringify(policy.redeem)},`,
1605
+ ` personField: ${JSON.stringify(policy.personField)},`,
1606
+ ` roleField: ${JSON.stringify(policy.roleField)},`,
1607
+ ` scopeField: ${JSON.stringify(policy.scopeField)},`,
1608
+ ` ),`,
1609
+ ]),
1610
+ ` ],`,
1611
+ ` );`,
1612
+ ]
1613
+ : []),
1614
+ ``,
1615
+ ` /// Mount the one application at the UI boundary and pass [application] down.`,
1616
+ ` ///`,
1617
+ ` /// This is the only application bootstrap tenant code should own.`,
1618
+ ` ///`,
1619
+ ` /// The object supplied to [builder] is the application state. Keep it as the only`,
1620
+ ` /// business-state dependency; do not recreate its records in providers, repositories or caches.`,
1621
+ ` static Widget nomosWidget({`,
1622
+ ` Key? key,`,
1623
+ ` required String cloud,`,
1624
+ ` required String application,`,
1625
+ ` required ApplicationSignIn signIn,`,
1626
+ ` required Widget Function(BuildContext context, ${application} application) builder,`,
1627
+ ` Widget Function(BuildContext context)? loadingBuilder,`,
1628
+ ` Widget Function(BuildContext context, Object error, VoidCallback retry)? errorBuilder,`,
1629
+ ` }) => NmsInternalApplicationHost.home<${application}>(`,
1630
+ ` key: key, cloud: cloud, application: application, signIn: signIn,`,
1631
+ ` bind: nomosInternalBind${application}, builder: builder,`,
1632
+ ` loadingBuilder: loadingBuilder, errorBuilder: errorBuilder,`,
1633
+ ` );`,
1634
+ `}`,
1635
+ ``,
1636
+ `Future<${application}> nomosInternalBind${application}(`,
1637
+ ` NomosSession session, NmsInternalApplicationUserBinding user,`,
1638
+ `) async => ${application}._(`,
1639
+ ` nmsSession: session,`,
1640
+ ...(hasInvitations ? [` nmsUser: user,`] : []),
1641
+ ` currentUser: NmsInternalUsers.create(identityId: user.nmsInternalIdentityId, identityAddress: user.nmsInternalIdentityAddress),`,
1642
+ ...members.map(({ member, application: child, prefix }) =>
1643
+ ` ${member}: await ${prefix}.nomosInternalBind${child}(session, user),`),
1644
+ `);`,
1645
+ ``,
1646
+ ].join("\n");
1647
+ }
1648
+
1649
+ function tenantDartExport(uri: string, src: string, publicFunctions: readonly string[] = []): string[] {
1650
+ const names = [...tenantDartExportNames(src), ...publicFunctions].sort();
1651
+ return names.length === 0 ? [] : [`export '${uri}' show ${names.join(", ")};`];
1652
+ }
1653
+
1088
1654
  /** `typedef <alias> = <generated>;` lines for the barrel, validated FAIL-CLOSED against the emitted
1089
1655
  * types surface: every target must exist, no alias may collide with a generated name or another alias. */
1090
1656
  function dartAliasLines(names: Readonly<Record<string, string>> | undefined, declared: ReadonlySet<string>): string[] {
@@ -1153,6 +1719,7 @@ function emitDartPackages(
1153
1719
  names: Readonly<Record<string, string>> | undefined,
1154
1720
  compat?: NomosCompat,
1155
1721
  describePackageName = packages.client,
1722
+ deployJsonPath?: string,
1156
1723
  ): { written: string[]; cursed: string[] } {
1157
1724
  for (const [role, name] of Object.entries(packages)) {
1158
1725
  if (typeof name !== "string" || !isPubPackageName(name)) {
@@ -1206,6 +1773,9 @@ function emitDartPackages(
1206
1773
  // banning the Dart SDK. Allow every dart:* import; keep the cross-package allowlist otherwise.
1207
1774
  !m[1]!.startsWith("dart:") &&
1208
1775
  m[1] !== "package:nomos_types/nomos_types.dart" &&
1776
+ m[1] !== "package:nomos_types/src/nomos_ref.dart" &&
1777
+ m[1] !== "package:nomos_types/src/foreign_ref.dart" &&
1778
+ m[1] !== "package:nomos_types/src/invitation.dart" &&
1209
1779
  m[1] !== `${FRAMEWORK_FILE}.dart` &&
1210
1780
  !siblingTypeFiles.has(m[1]!)
1211
1781
  ) {
@@ -1228,7 +1798,6 @@ function emitDartPackages(
1228
1798
  if (packageContracts.length > 0) {
1229
1799
  writeTypes(CONTRACTS_FILE, generateDartContracts(packageContracts, {
1230
1800
  ...(shared.frameworkMod !== undefined ? { frameworkImportUri: `${FRAMEWORK_FILE}.dart` } : {}),
1231
- aggregateIdClassNames: packageAggregateIdClassNames(modules),
1232
1801
  }));
1233
1802
  }
1234
1803
  // ACTIVITY + DESCRIPTION REGISTRY (layered): universal, including packages with empty history.
@@ -1258,7 +1827,7 @@ function emitDartPackages(
1258
1827
  }
1259
1828
  }
1260
1829
  // AUTO cross-domain `idOf(Agg)` imports (dart_codegen_parity.md P2): a payload field typed with a
1261
- // SIBLING domain's typed id class needs that domain's TYPES file (a permitted sibling import).
1830
+ // SIBLING aggregate's generated type needs that domain's TYPES file (a permitted sibling import).
1262
1831
  for (const target of collectPayloadIdOfTargets(dd.modForDart.directives)) {
1263
1832
  const tf = aggIdToTypeFile.get(target);
1264
1833
  if (tf !== undefined && tf !== dd.modForDart.name) frImports.add(`${tf}.dart`);
@@ -1272,6 +1841,7 @@ function emitDartPackages(
1272
1841
  mod.name,
1273
1842
  generateDartDomain(modForTypes, {
1274
1843
  ...dd.skips,
1844
+ packageAggregateDomains: buildPackageAggregateDomains(modules),
1275
1845
  ...(dd.foldsFramework ? { frameworkImportUri: `${FRAMEWORK_FILE}.dart` } : {}),
1276
1846
  }),
1277
1847
  );
@@ -1288,6 +1858,14 @@ function emitDartPackages(
1288
1858
  // but a file no alias references needs no import (the `export` re-exports it). Importing every file
1289
1859
  // produced unused_import warnings in the tenant's barrel (co2 feedback).
1290
1860
  const aliasTargets = new Set(Object.keys(names ?? {}));
1861
+ const frameworkAliasTargets = [...aliasTargets].filter((name) =>
1862
+ (declaredByFile.get(FRAMEWORK_FILE) ?? new Set<string>()).has(name));
1863
+ if (frameworkAliasTargets.length > 0) {
1864
+ fail(
1865
+ `dart.names cannot publish Nomos implementation types (${frameworkAliasTargets.sort().join(", ")}) — ` +
1866
+ `name business concepts declared by the tenant domains instead`,
1867
+ );
1868
+ }
1291
1869
  const aliasImportUris = [...typesSources.keys()]
1292
1870
  .filter((f) => [...(declaredByFile.get(f) ?? [])].some((n) => aliasTargets.has(n)))
1293
1871
  .map((f) => `${genRel}/${f}.dart`);
@@ -1308,6 +1886,37 @@ function emitDartPackages(
1308
1886
  const typesExportKeys = explicitBarrel
1309
1887
  ? [...typesSources.keys()].filter((f) => !domainFileBases.has(f))
1310
1888
  : [...typesSources.keys()];
1889
+ // Framework-shared types exist only so generated implementation parts can
1890
+ // compile. They are not tenant vocabulary and must never enter either
1891
+ // public package barrel (SignerDelegation/EnrollDelegationPayload and the
1892
+ // like otherwise become tempting autocomplete footguns).
1893
+ const tenantTypesExportKeys = typesExportKeys.filter((f) => f !== FRAMEWORK_FILE);
1894
+ const frameworkSource = typesSources.get(FRAMEWORK_FILE) ?? "";
1895
+ // Shared business value objects may be emitted into the private framework
1896
+ // implementation file because several domains reuse them. Promote those
1897
+ // shared values and their transitive variant members, but mechanically
1898
+ // exclude every framework aggregate/directive family and Nomos-prefixed type.
1899
+ const sharedBusinessTypeNames = tenantDartTypeNames(frameworkSource).filter(
1900
+ (name) =>
1901
+ !name.startsWith("Nomos") &&
1902
+ ![...FRAMEWORK_AGGREGATE_IDS].some((id) => name.startsWith(id)) &&
1903
+ ![...FRAMEWORK_DIRECTIVE_IDS].some((id) => name === `${id[0]!.toUpperCase()}${id.slice(1)}Payload`),
1904
+ );
1905
+ const businessSharedFile = "business_shared.dart";
1906
+ const businessSharedPath = path.join(typesGen, businessSharedFile);
1907
+ writeFileSync(
1908
+ businessSharedPath,
1909
+ [
1910
+ "// GENERATED by nomos-compile — shared business values and activity; no runtime machinery.",
1911
+ `export 'package:nomos_types/nomos_types.dart' show NomosRef, Identity, User, Recipient, Invitation, ActivityEvent, ActivityRef, ActivitySeverity, SurfacedObservation, SurfacedValue, SurfacedResolved, SurfacedPending, SurfacedEvolutionRequired;`,
1912
+ ...(sharedBusinessTypeNames.length === 0
1913
+ ? []
1914
+ : [`export '${FRAMEWORK_FILE}.dart' show ${sharedBusinessTypeNames.join(", ")};`]),
1915
+ "",
1916
+ ].join("\n"),
1917
+ "utf8",
1918
+ );
1919
+ written.push(businessSharedPath);
1311
1920
  const explicitBarrelNote = explicitBarrel
1312
1921
  ? [
1313
1922
  `// barrelExports: 'explicit' — per-domain aggregate/read-model classes are NOT flat-exported here.`,
@@ -1319,7 +1928,7 @@ function emitDartPackages(
1319
1928
  // under `barrelExports: 'explicit'` per-domain files are withheld, so they cannot collide there.
1320
1929
  try {
1321
1930
  assertNoDartBarrelCollisions(
1322
- typesExportKeys.map((f): BarrelExportedFile => ({ label: f, src: typesSources.get(f) ?? "" })),
1931
+ tenantTypesExportKeys.map((f): BarrelExportedFile => ({ label: f, src: typesSources.get(f) ?? "" })),
1323
1932
  );
1324
1933
  } catch (e) {
1325
1934
  fail((e as Error).message);
@@ -1332,8 +1941,13 @@ function emitDartPackages(
1332
1941
  "",
1333
1942
  ...explicitBarrelNote,
1334
1943
  ...aliasImportLines(barrelNames, barrelAliasImportUris),
1335
- `export 'package:nomos_types/nomos_types.dart';`,
1336
- ...typesExportKeys.map((f) => `export '${genRel}/${f}.dart';`),
1944
+ `export '${genRel}/${businessSharedFile}';`,
1945
+ ...tenantTypesExportKeys.flatMap((f) =>
1946
+ tenantDartExport(
1947
+ `${genRel}/${f}.dart`,
1948
+ typesSources.get(f) ?? "",
1949
+ f === ACTIVITY_FILE ? ["renderActivity", "activityNarrative"] : [],
1950
+ )),
1337
1951
  ...dartAliasLines(barrelNames, declared),
1338
1952
  "",
1339
1953
  ];
@@ -1358,8 +1972,17 @@ function emitDartPackages(
1358
1972
  // ── the CLIENT package — effectful runtime, depends on types + nomos_client ──
1359
1973
  const clientGen = freshGenDir(packages.client);
1360
1974
  const typesImportUri = `package:${packages.types}/${packages.types}.dart`;
1975
+ const implementationTypeImports = explicitBarrel
1976
+ ? modules.map((m) => `package:${packages.types}/${genRel}/${m.name}.dart`)
1977
+ : [];
1978
+ const frameworkImplementationImport = typesSources.has(FRAMEWORK_FILE)
1979
+ ? `package:${packages.types}/${genRel}/${FRAMEWORK_FILE}.dart`
1980
+ : undefined;
1981
+ const frameworkTypeNames = frameworkImplementationImport === undefined
1982
+ ? []
1983
+ : [...declaredDartTypeNames(typesSources.get(FRAMEWORK_FILE) ?? "")];
1361
1984
  for (const { mod, dd } of perDomain) {
1362
- const clientSrc = generateDartBridgeClient(dd.modForDart, {
1985
+ const makeClient = (imports: readonly string[]) => generateDartBridgeClient(dd.modForDart, {
1363
1986
  domainHash,
1364
1987
  interface: packageInterface[dd.modForDart.domain ?? dd.modForDart.name],
1365
1988
  ...(compat !== undefined ? { compat } : {}),
@@ -1367,27 +1990,121 @@ function emitDartPackages(
1367
1990
  typesImportUri,
1368
1991
  emitActivity: true,
1369
1992
  describeModel,
1370
- packageAggregateDomains: buildPackageAggregateDomains(modules),
1993
+ // Cross-domain record traversal lives on the application facade. Keeping
1994
+ // the package map out of the private client prevents sibling client imports.
1371
1995
  // Under 'explicit' the types barrel withholds per-domain classes — import every domain's TYPES
1372
1996
  // file directly so the client's read-model/payload/ref/typed-id references resolve. (The no-dup-
1373
1997
  // symbol guarantee across domain files means an over-broad set never introduces ambiguity.)
1374
- ...(explicitBarrel
1375
- ? { explicitTypeImports: modules.map((m) => `package:${packages.types}/${genRel}/${m.name}.dart`) }
1998
+ ...(imports.length > 0
1999
+ ? { explicitTypeImports: imports }
1376
2000
  : {}),
1377
2001
  });
1378
- const cp = path.join(clientGen, `${mod.name}.client.dart`);
1379
- writeFileSync(cp, clientSrc, "utf8");
2002
+ const makeApplication = (imports: readonly string[]) => generateDartApplication(dd.modForDart, {
2003
+ supportImport: "package",
2004
+ typesImportUri,
2005
+ packageAggregateDomains: buildPackageAggregateDomains(modules),
2006
+ flutterHost: true,
2007
+ tenantRoot: perDomain.length === 1,
2008
+ ...(imports.length > 0
2009
+ ? { explicitTypeImports: imports }
2010
+ : {}),
2011
+ });
2012
+ let clientSrc = makeClient(implementationTypeImports);
2013
+ let applicationSrc = makeApplication(implementationTypeImports);
2014
+ // Import the private framework types file only into implementation libraries
2015
+ // that actually name one of its declarations. Ignore the embedded describe
2016
+ // JSON while scanning so framework words in documentation do not manufacture
2017
+ // an otherwise-unused import.
2018
+ const implementationSource = `${clientSrc}\n${applicationSrc}`.replace(/r'''[\s\S]*?'''/g, "");
2019
+ const needsFrameworkTypes = frameworkImplementationImport !== undefined &&
2020
+ frameworkTypeNames.some((name) => new RegExp(`\\b${name}\\b`).test(implementationSource));
2021
+ if (needsFrameworkTypes) {
2022
+ const imports = [...implementationTypeImports, frameworkImplementationImport];
2023
+ clientSrc = makeClient(imports);
2024
+ applicationSrc = makeApplication(imports);
2025
+ }
2026
+ const clientFile = `${mod.name}.client.dart`;
2027
+ const applicationFile = `${mod.name}.application.dart`;
2028
+ const encapsulated = encapsulateDartClient(
2029
+ clientSrc,
2030
+ applicationSrc,
2031
+ clientFile,
2032
+ applicationFile,
2033
+ );
2034
+ const cp = path.join(clientGen, clientFile);
2035
+ writeFileSync(cp, encapsulated.clientPart, "utf8");
1380
2036
  written.push(cp);
2037
+ const ap = path.join(clientGen, applicationFile);
2038
+ writeFileSync(ap, encapsulated.applicationLibrary, "utf8");
2039
+ written.push(ap);
2040
+ }
2041
+ const packageApplicationFile = `${describePackageName}.package.application`;
2042
+ if (perDomain.length > 1) {
2043
+ const topology = resolveApplicationTopology(modules);
2044
+ const packageApplicationPath = path.join(clientGen, `${packageApplicationFile}.dart`);
2045
+ writeFileSync(
2046
+ packageApplicationPath,
2047
+ generatedPackageApplication(
2048
+ describePackageName,
2049
+ perDomain.map(({ mod, dd }) => ({
2050
+ domain: dd.modForDart.domain ?? dd.modForDart.name,
2051
+ file: mod.name,
2052
+ invitationPolicies: (dd.modForDart.applicationWorkspaceSharing ?? []).map((policy) => ({
2053
+ domainHash,
2054
+ query: policy.invitationLaw.pendingByRecipient.id,
2055
+ redeem: policy.invitationLaw.redeem.id,
2056
+ personField: policy.person,
2057
+ roleField: policy.role,
2058
+ scopeField: policy.scope,
2059
+ })),
2060
+ })),
2061
+ topology.homeDomain,
2062
+ topology.rootDomains,
2063
+ ),
2064
+ "utf8",
2065
+ );
2066
+ written.push(packageApplicationPath);
1381
2067
  }
1382
2068
  const clientBarrel = [
1383
2069
  "// GENERATED by nomos-compile — the CLIENT package's public entrypoint. DO NOT EDIT.",
1384
- "// Effectful runtime clients (offer methods over NomosBridge). Composition/runtime layer",
1385
- "// only — UI code should depend on the sibling types package, not on this.",
2070
+ `/// The generated business application for ${describePackageName}.`,
2071
+ `///`,
2072
+ `/// Import this library, mount [${dartApplicationClassName(describePackageName)}.nomosWidget] once, and pass the`,
2073
+ `/// supplied [${dartApplicationClassName(describePackageName)}] through the UI. Its typed actions, records,`,
2074
+ `/// watches, references and status are the canonical application state. Do not recreate them`,
2075
+ `/// in tenant repositories, providers, sessions, clients or caches.`,
2076
+ `///`,
2077
+ `/// Nomos privately owns persistence, offline work, synchronization, identity, access,`,
2078
+ `/// provenance, conflicts, schema evolution and recovery. Framework implementation details`,
2079
+ `/// are deliberately not exported here.`,
1386
2080
  "library;",
1387
2081
  "",
1388
- `export 'package:nomos_client/nomos_client.dart';`,
1389
- `export '${typesImportUri}';`,
1390
- ...perDomain.map(({ mod }) => `export '${genRel}/${mod.name}.client.dart';`),
2082
+ `export 'package:${packages.types}/${genRel}/${businessSharedFile}';`,
2083
+ ...tenantTypesExportKeys
2084
+ .map((file) => {
2085
+ const names = tenantDartExportNames(typesSources.get(file) ?? "");
2086
+ return names.length === 0
2087
+ ? null
2088
+ : `export 'package:${packages.types}/${genRel}/${file}.dart' show ${names.join(", ")};`;
2089
+ })
2090
+ .filter((line): line is string => line !== null),
2091
+ ...(barrelNames !== undefined && Object.keys(barrelNames).length > 0
2092
+ ? [`export 'package:${packages.types}/${packages.types}.dart' show ${Object.values(barrelNames).sort().join(", ")};`]
2093
+ : []),
2094
+ ...(perDomain.length > 1
2095
+ ? [
2096
+ `export 'package:nomos_flutter/src/application_status.dart' show ApplicationSignIn, ApplicationStatus, ApplicationStatusPhase;`,
2097
+ `export '${genRel}/${packageApplicationFile}.dart' show ${dartApplicationClassName(describePackageName)};`,
2098
+ ...perDomain.map(({ mod, dd }) => {
2099
+ const packageClass = dartApplicationClassName(describePackageName);
2100
+ const publicNames = dartApplicationPublicClassNames(dd.modForDart).filter(
2101
+ (name) => name !== packageClass,
2102
+ );
2103
+ return `export '${genRel}/${mod.name}.application.dart' show ${publicNames.join(", ")};`;
2104
+ }),
2105
+ ]
2106
+ : perDomain.map(({ mod, dd }) =>
2107
+ `export '${genRel}/${mod.name}.application.dart' show ${dartApplicationPublicClassNames(dd.modForDart, { tenantRoot: true }).join(", ")};`)),
1391
2108
  "",
1392
2109
  ];
1393
2110
  const clientBarrelPath = path.join(pkgDir(packages.client), "lib", `${packages.client}.dart`);
@@ -1397,31 +2114,82 @@ function emitDartPackages(
1397
2114
  pkgDir(packages.client),
1398
2115
  [
1399
2116
  `name: ${packages.client}`,
1400
- `description: Generated Nomos domain CLIENTS — the effectful runtime layer over NomosBridge.`,
2117
+ `description: Generated Nomos business application APIs.`,
1401
2118
  `version: 0.1.0`,
1402
2119
  `publish_to: none # tenant-owned: remove to publish`,
1403
2120
  `environment:`,
1404
2121
  ` sdk: ^3.5.0`,
2122
+ ` flutter: ">=3.10.0"`,
1405
2123
  `dependencies:`,
2124
+ ` flutter:`,
2125
+ ` sdk: flutter`,
1406
2126
  ` nomos_client: ${NOMOS_CLIENT_DEP}`,
2127
+ ` nomos_flutter: ${NOMOS_FLUTTER_DEP}`,
1407
2128
  ` ${packages.types}:`,
1408
2129
  ` path: ../${packages.types} # co-emitted sibling; replace with a hosted constraint when publishing`,
1409
2130
  ``,
1410
2131
  ].join("\n"),
1411
2132
  );
1412
2133
 
1413
- // ── the TEST package (optional) — the headless-harness surface, test-only dependency ──
2134
+ // ── the TEST package (optional) — generated business scenarios, test-only dependency ──
1414
2135
  if (packages.test !== undefined) {
1415
2136
  const testGen = freshGenDir(packages.test);
1416
- void testGen; // generated fixtures land here as they grow; the barrel is the surface today
2137
+ if (deployJsonPath === undefined) fail("dart.packages.test requires the compiled application package");
2138
+ const embeddedDeployName = `${describePackageName}.deploy.json`;
2139
+ const embeddedDeployPath = path.join(testGen, embeddedDeployName);
2140
+ copyFileSync(deployJsonPath, embeddedDeployPath);
2141
+ written.push(embeddedDeployPath);
2142
+ const scenarioSurfaces = perDomain.length > 1
2143
+ ? (() => {
2144
+ const topology = resolveApplicationTopology(modules);
2145
+ const home = perDomain.find(({ dd }) =>
2146
+ (dd.modForDart.domain ?? dd.modForDart.name) === topology.homeDomain,
2147
+ );
2148
+ if (home === undefined) {
2149
+ fail(`generated Dart scenario cannot find its home domain '${topology.homeDomain}'`);
2150
+ }
2151
+ return [{
2152
+ mod: home.mod,
2153
+ dd: home.dd,
2154
+ file: describePackageName,
2155
+ surfaceName: describePackageName,
2156
+ applicationFile: packageApplicationFile,
2157
+ }];
2158
+ })()
2159
+ : perDomain.map(({ mod, dd }) => ({
2160
+ mod,
2161
+ dd,
2162
+ file: mod.name,
2163
+ surfaceName: dd.modForDart.domain ?? dd.modForDart.name,
2164
+ applicationFile: `${mod.name}.application`,
2165
+ }));
2166
+ for (const { mod, dd, file, surfaceName, applicationFile } of scenarioSurfaces) {
2167
+ const scenarioPath = path.join(testGen, `${file}.scenario.dart`);
2168
+ writeFileSync(
2169
+ scenarioPath,
2170
+ generateDartScenario(
2171
+ dd.modForDart,
2172
+ packages.client,
2173
+ packages.test,
2174
+ embeddedDeployName,
2175
+ {
2176
+ surfaceName,
2177
+ applicationFile,
2178
+ applicationModules: perDomain.map(({ dd: candidate }) => candidate.modForDart),
2179
+ },
2180
+ ),
2181
+ "utf8",
2182
+ );
2183
+ written.push(scenarioPath);
2184
+ }
1417
2185
  const testBarrel = [
1418
2186
  "// GENERATED by nomos-compile — the TEST package's public entrypoint. DO NOT EDIT.",
1419
- "// The headless-harness surface (NomosTestHarness + the node-runner transport) plus the",
1420
- "// full client layer, for widget/integration tests. Depend on this ONLY from dev_dependencies.",
2187
+ "// Business scenarios drive the exact generated production application; framework machinery stays private.",
1421
2188
  "library;",
1422
2189
  "",
1423
- `export 'package:nomos_client/testing.dart';`,
1424
2190
  `export 'package:${packages.client}/${packages.client}.dart';`,
2191
+ ...scenarioSurfaces.map(({ file, surfaceName }) =>
2192
+ `export '${genRel}/${file}.scenario.dart' show ${dartScenarioClassName(surfaceName)}, ${dartTestPersonClassName(surfaceName)}, ${dartScenarioGivenClassName(surfaceName)};`),
1425
2193
  "",
1426
2194
  ];
1427
2195
  const testBarrelPath = path.join(pkgDir(packages.test), "lib", `${packages.test}.dart`);
@@ -1431,15 +2199,22 @@ function emitDartPackages(
1431
2199
  pkgDir(packages.test),
1432
2200
  [
1433
2201
  `name: ${packages.test}`,
1434
- `description: Generated Nomos test surface — headless harness + clients. dev_dependencies only.`,
2202
+ `description: Generated business application scenarios. dev_dependencies only.`,
1435
2203
  `version: 0.1.0`,
1436
2204
  `publish_to: none # tenant-owned: remove to publish`,
1437
2205
  `environment:`,
1438
2206
  ` sdk: ^3.5.0`,
1439
2207
  `dependencies:`,
2208
+ ` flutter:`,
2209
+ ` sdk: flutter`,
1440
2210
  ` nomos_client: ${NOMOS_CLIENT_DEP}`,
1441
2211
  ` ${packages.client}:`,
1442
2212
  ` path: ../${packages.client} # co-emitted sibling; replace with a hosted constraint when publishing`,
2213
+ `dev_dependencies:`,
2214
+ ` test: ^1.25.0`,
2215
+ `flutter:`,
2216
+ ` assets:`,
2217
+ ` - lib/${genRel}/${embeddedDeployName}`,
1443
2218
  ``,
1444
2219
  ].join("\n"),
1445
2220
  );
@@ -1519,7 +2294,15 @@ function emitDartPackages(
1519
2294
 
1520
2295
  async function main(): Promise<void> {
1521
2296
  const argv = process.argv.slice(2);
1522
- const layeredFlag = argv.includes("--layered");
2297
+ const supportedFlags = new Set(["--require-usdcat", "--compact", "--analyze"]);
2298
+ for (const flag of argv.filter((arg) => arg.startsWith("--"))) {
2299
+ if (supportedFlags.has(flag)) continue;
2300
+ if (flag === "--layered") {
2301
+ fail("--layered no longer exists: every package is a USD layer stack and every compile proves its flattened law");
2302
+ }
2303
+ fail(`unknown compile flag '${flag}'`);
2304
+ }
2305
+ const requireUsdcat = argv.includes("--require-usdcat") || process.env.NOMOS_REQUIRE_USDCAT === "1";
1523
2306
  const compactFlag = argv.includes("--compact") || process.env["NOMOS_COMPACT"] === "1";
1524
2307
  // §"Shipped (1–4)" follow-up: `githolon compile --analyze` (or NOMOS_ANALYZE=1) runs
1525
2308
  // `dart analyze` on the generated Dart package after emission and fails the compile on red.
@@ -1549,11 +2332,29 @@ async function main(): Promise<void> {
1549
2332
  }
1550
2333
  } catch { /* no dsl resolvable from the law dir (vendored/workspace setups) — nothing to compare */ }
1551
2334
 
1552
- const cfgModule = (await import(pathToFileURL(configPath).href)) as { default?: PackageConfig };
1553
- const cfg = cfgModule.default;
2335
+ const cfgModule = (await import(pathToFileURL(configPath).href)) as { default?: unknown };
2336
+ const configValue = cfgModule.default;
2337
+ if (isApplicationCompositionConfig(configValue)) {
2338
+ try {
2339
+ const emitted = compileApplicationComposition(configValue, configPath, { requireUsdcat });
2340
+ console.log([
2341
+ `${configValue.application} — compiled Nomos application composition`,
2342
+ `stage: ${emitted.outputPath}`,
2343
+ `assets: ${emitted.referencedAssets.length} local content-addressed reference(s)`,
2344
+ `runtime configuration: USD (the authoring .mjs does not travel)`,
2345
+ ].join("\n"));
2346
+ return;
2347
+ } catch (error) {
2348
+ fail(`application composition refused: ${(error as Error).message}`);
2349
+ }
2350
+ }
2351
+ const cfg = configValue as PackageConfig | undefined;
1554
2352
  if (!cfg || typeof cfg.name !== "string" || !Array.isArray(cfg.domains) || cfg.domains.length === 0) {
1555
2353
  fail(`config must default-export { name, domains: [{ key, modules }, ...] }`);
1556
2354
  }
2355
+ if (Object.prototype.hasOwnProperty.call(cfg, "layered")) {
2356
+ fail("nomos.package.mjs no longer accepts 'layered': every package is layered, with no alternate compile lane");
2357
+ }
1557
2358
  // ANY readable name works ("tool-library", "tool_library", "Tool Library"):
1558
2359
  // artifact FILENAMES keep it verbatim; generated SYMBOLS normalize it
1559
2360
  // (toolLibraryClient, TOOL_LIBRARY_DOMAIN_HASH — see codegen_ts.ts).
@@ -1561,22 +2362,31 @@ async function main(): Promise<void> {
1561
2362
  fail(`package name '${cfg.name}' is not a safe basename (letters, digits, spaces, '.', '-', '_')`);
1562
2363
  }
1563
2364
 
1564
- // ONE BUSINESS BIRTH IS A COMPLETE PRODUCT ENTRYPOINT. The compiler privately appends the
1565
- // application/home law which turns a signed-in identity into its home workspace; that home can then
1566
- // call the generated business birth. Tenant config never names this domain. The deliberately obscure
1567
- // key reduces collision likelihood, but collision handling is still fail-closed: never reinterpret a
1568
- // tenant's same-named concept as framework machinery.
2365
+ // EVERY GENERATED APPLICATION IS A COMPLETE PRODUCT ENTRYPOINT. The compiler privately appends the
2366
+ // authority bootstrap law that turns verified credential evidence into durable device authorship. Tenant
2367
+ // config never names this domain and presentation filters it back out: a package cannot emit the generated
2368
+ // application facade while omitting the private machinery needed to open it. The deliberately obscure key
2369
+ // reduces collision likelihood, but collision handling is still fail-closed: never reinterpret a tenant's
2370
+ // same-named concept as framework machinery.
1569
2371
  if (cfg.domains.some((d) => d.key === APPLICATION_HOME_DOMAIN)) {
1570
2372
  fail(
1571
- `domain key '${APPLICATION_HOME_DOMAIN}' is reserved by Nomos for the private application/home law; ` +
2373
+ `domain key '${APPLICATION_HOME_DOMAIN}' is reserved by Nomos for private application authority law; ` +
1572
2374
  `rename the tenant domain (Nomos will not silently overwrite or reroute it)`,
1573
2375
  );
1574
2376
  }
1575
- const domainsToCompile: DomainConfig[] = [...cfg.domains];
1576
- let applicationHomeInjected = false;
2377
+ const domainsToCompile: DomainConfig[] = [
2378
+ ...cfg.domains,
2379
+ {
2380
+ key: APPLICATION_HOME_DOMAIN,
2381
+ modules: [path.join(DSL_DIR, "src", "framework", "application_home.ts")],
2382
+ },
2383
+ ];
1577
2384
 
1578
2385
  const outDir = path.resolve(cfgDir, cfg.outDir ?? "build");
1579
2386
  mkdirSync(outDir, { recursive: true });
2387
+ // The package-wide executable lane no longer exists. Remove its ignored build artifact on every
2388
+ // native compile so a developer cannot accidentally inspect or execute stale bytes as canonical law.
2389
+ rmSync(path.join(outDir, ".nomos_lump.js"), { force: true });
1580
2390
 
1581
2391
  // ── import + compose each domain ──
1582
2392
  // LAZY PER-DOMAIN BOOT (#34): the generated entry registers each domain's modules
@@ -1585,12 +2395,10 @@ async function main(): Promise<void> {
1585
2395
  // per-plan sandbox boots ONLY the domain the dispatched intent touches. The
1586
2396
  // routing table for type/relation-keyed dispatches is computed below by the
1587
2397
  // SAME scans the engine registries run (`collectEngineRouting` — one machinery).
1588
- // `NOMOS_LUMP_BOOT=eager` keeps the pre-#34 whole-law top-level boot (static
1589
- // imports, no routing) — the equivalence baseline the #34 suite compares against.
1590
- const eagerLump = process.env.NOMOS_LUMP_BOOT === "eager";
1591
- const entryImports: string[] = [];
1592
- const entryDomains: string[] = [];
1593
- const entryMintInstalls: string[] = []; // route-tagged in-plan mints (taxonomy packages only)
2398
+ // Every executable is an independently addressable lazy plan asset. There is no package-wide
2399
+ // eager/monolithic execution mode: keeping that alternative would let it become canonical again.
2400
+ const entryDomainSourceByKey = new Map<string, string>();
2401
+ const entryMintInstallByKey = new Map<string, string>();
1594
2402
  const routingInput: Record<string, Mod[]> = {};
1595
2403
  const domainModules: DomainModule[] = [];
1596
2404
  // THE SELF-DESCRIBE DECLS (G1): per domain key, the FINISHED plain read/maintenance decls the
@@ -1599,9 +2407,7 @@ async function main(): Promise<void> {
1599
2407
  // pass finished decls through), serialized into the generated entry as `openapiDecls`, so the
1600
2408
  // kernel-run describe reproduces THIS compile's `<name>.openapi.json` byte-for-byte.
1601
2409
  const openapiDeclsByDomain: Record<string, unknown> = {};
1602
- const layered = layeredFlag || cfg.layered === true;
1603
2410
  const layeredDomains: { key: string; inputs: LayeredModuleInput[] }[] = [];
1604
- let importSeq = 0;
1605
2411
  /**
1606
2412
  * THE CAPABILITY LOWERING (capability_marketplace.md §4 — the deploy-warning
1607
2413
  * prerequisite + the executor's task→capability→domain map). Rides the
@@ -1633,29 +2439,10 @@ async function main(): Promise<void> {
1633
2439
  if (!existsSync(abs)) fail(`domain '${d.key}': module not found: ${abs}`);
1634
2440
  const m = (await import(pathToFileURL(abs).href)) as Mod;
1635
2441
  mods.push(m);
1636
- if (eagerLump) {
1637
- const v = `m${importSeq++}`;
1638
- sourceExprs.push(v);
1639
- entryImports.push(`import * as ${v} from ${JSON.stringify(abs)};`);
1640
- } else {
1641
- sourceExprs.push(`() => require(${JSON.stringify(abs)})`);
1642
- }
2442
+ sourceExprs.push(`() => require(${JSON.stringify(abs)})`);
1643
2443
  }
1644
2444
  let merged: Mod = {};
1645
2445
  for (const m of mods) merged = { ...merged, ...m };
1646
- const declaresBusinessBirth = Object.values(merged).some(
1647
- (value) =>
1648
- value !== null &&
1649
- typeof value === "object" &&
1650
- (value as { __isBusinessBirth?: unknown }).__isBusinessBirth === true,
1651
- );
1652
- if (declaresBusinessBirth && !applicationHomeInjected) {
1653
- applicationHomeInjected = true;
1654
- domainsToCompile.push({
1655
- key: APPLICATION_HOME_DOMAIN,
1656
- modules: [path.join(DSL_DIR, "src", "framework", "application_home.ts")],
1657
- });
1658
- }
1659
2446
  merged = expandBusinessBirthExports(merged, d.key);
1660
2447
  merged = withOwnedCustodyExports(merged);
1661
2448
  // `capability()` bundles expand into discoverable entries (the CLI plane's
@@ -1755,29 +2542,18 @@ async function main(): Promise<void> {
1755
2542
  shardingMins = shardingExtrema("min");
1756
2543
  shardingMaxes = shardingExtrema("max");
1757
2544
  const specJson = JSON.stringify(spec);
1758
- if (eagerLump) {
1759
- if (!entryImports.some((l) => l.includes("workspace-sharding"))) {
1760
- entryImports.push(
1761
- `import { shardingLawModule as __nomosShardingLaw, installRouteTaggedMint as __nomosInstallMint } from "@githolon/dsl/workspace-sharding";`,
1762
- );
1763
- }
1764
- sourceExprs.push(`__nomosShardingLaw(${specJson})`);
1765
- } else {
1766
- shardingLazyExpr = `() => (require("@githolon/dsl/workspace-sharding") as any).shardingLawModule(${specJson})`;
1767
- sourceExprs.push(shardingLazyExpr);
1768
- }
2545
+ shardingLazyExpr = `() => (require("@githolon/dsl/workspace-sharding") as any).shardingLawModule(${specJson})`;
2546
+ sourceExprs.push(shardingLazyExpr);
1769
2547
  // ROUTE-TAGGED IN-PLAN MINTS (§4, slice 3): installed AFTER registerEngine at
1770
2548
  // the lump's top level — pre-freeze, taxonomy packages only (a taxonomy-free
1771
2549
  // lump never imports the module: the slice-1 hash-stability law).
1772
- entryMintInstalls.push(
1773
- eagerLump
1774
- ? `__nomosInstallMint(${specJson});`
1775
- : `(require("@githolon/dsl/workspace-sharding") as any).installRouteTaggedMint(${specJson});`,
1776
- );
2550
+ const mintInstall = `(require("@githolon/dsl/workspace-sharding") as any).installRouteTaggedMint(${specJson});`;
2551
+ entryMintInstallByKey.set(d.key, mintInstall);
1777
2552
  }
1778
2553
  }
1779
2554
 
1780
- entryDomains.push(` ${JSON.stringify(d.key)}: [${sourceExprs.join(", ")}],`);
2555
+ const domainSource = ` ${JSON.stringify(d.key)}: [${sourceExprs.join(", ")}],`;
2556
+ entryDomainSourceByKey.set(d.key, domainSource);
1781
2557
  routingInput[d.key] = mods;
1782
2558
  // ── THE AUX INVARIANT-ROUTING KEY (#47, sharding slice 7 — the invariant-gate
1783
2559
  // flame pass): in a LAZY lump, the derived sharding law registers a SECOND
@@ -1791,14 +2567,15 @@ async function main(): Promise<void> {
1791
2567
  // canonical manifests are untouched (identity hashes do not move); eager
1792
2568
  // lumps are untouched (no routing table — the pre-#34 equivalence shape);
1793
2569
  // taxonomy-free packages emit NOTHING here (the slice-1 byte-stability law).
1794
- if (shardingModForDomain !== undefined && shardingLazyExpr !== undefined && !eagerLump) {
2570
+ if (shardingModForDomain !== undefined && shardingLazyExpr !== undefined) {
1795
2571
  const auxKey = `${d.key}/nomos-sharding`;
1796
2572
  if (domainsToCompile.some((x) => x.key === auxKey) || routingInput[auxKey] !== undefined) {
1797
2573
  fail(
1798
2574
  `domain key '${auxKey}' is reserved for the derived sharding law's invariant routing — rename the domain`,
1799
2575
  );
1800
2576
  }
1801
- entryDomains.push(` ${JSON.stringify(auxKey)}: [${shardingLazyExpr}],`);
2577
+ const auxSource = ` ${JSON.stringify(auxKey)}: [${shardingLazyExpr}],`;
2578
+ entryDomainSourceByKey.set(auxKey, auxSource);
1802
2579
  routingInput[d.key] = mods.filter((m) => m !== shardingModForDomain);
1803
2580
  routingInput[auxKey] = [shardingModForDomain];
1804
2581
  }
@@ -1833,7 +2610,7 @@ async function main(): Promise<void> {
1833
2610
  const usesRelations =
1834
2611
  dirsAll.some((dir) => (dir as { requiresRelationGate?: unknown }).requiresRelationGate !== undefined) ||
1835
2612
  dirsAll.some((dir) => (dir as { aggregateId?: string }).aggregateId === RelationTuple.id) ||
1836
- discover<RelationTypeDecl>(merged, isRelationTypeDecl).length > 0;
2613
+ discoverRelationTypeDeclarations(merged).length > 0;
1837
2614
  if (usesRelations && !queries.some((q) => q.id === relationTuplesByObject.id)) {
1838
2615
  queries.push(relationTuplesByObject);
1839
2616
  }
@@ -1870,11 +2647,11 @@ async function main(): Promise<void> {
1870
2647
  ...directivesOf(merged).flatMap((dir) => synthesizeBirthQuotaInvariants(dir)),
1871
2648
  ];
1872
2649
  // Native Zanzibar relation schema (P1) — auto-discovered by brand, like workspaceInvariants.
1873
- const relationTypes = [
1874
- ...discover<RelationTypeDecl>(merged, isRelationTypeDecl),
2650
+ const relationTypeFragments = [
2651
+ ...discoverRelationTypeDeclarations(merged),
1875
2652
  ...resolveNamed<RelationTypeDecl>(merged, d.relationTypes, "relationTypes"),
1876
2653
  ];
1877
- for (const rt of relationTypes) validateRelationType(rt); // DX fast-fail (Rust resolve is authoritative)
2654
+ const relationTypes = mergeRelationTypeDeclarations(relationTypeFragments);
1878
2655
  // Sums are AUTO-DISCOVERED like queries/counts (#57 — an exported SumDecl used to be
1879
2656
  // SILENTLY DROPPED unless config-named; the config lane stays as the escape hatch).
1880
2657
  // Reference-dedupe: a sum that is BOTH exported and config-named is the same object.
@@ -1942,7 +2719,9 @@ async function main(): Promise<void> {
1942
2719
  ...(maxes.length > 0 ? { maxes } : {}),
1943
2720
  ...(workspaceTypes.length > 0 ? { workspaceTypes } : {}),
1944
2721
  ...(workspaceInvariants.length > 0 ? { workspaceInvariants } : {}),
1945
- ...(relationTypes.length > 0 ? { relationTypes } : {}),
2722
+ // Relation declarations are already present in `modules` (including config-named exports), and
2723
+ // composeDomainModule performs the authoritative discovery + fragment union after expanding role
2724
+ // sugar. Re-threading the compiler's independently expanded copy here would invent a second author.
1946
2725
  ...(spatials.length > 0 ? { spatials } : {}),
1947
2726
  ...(d.dartImports !== undefined && d.dartImports.length > 0
1948
2727
  ? { dartImports: d.dartImports }
@@ -2033,17 +2812,20 @@ async function main(): Promise<void> {
2033
2812
  }
2034
2813
  }
2035
2814
 
2036
- // ── opt-in: ONE USD LAYER PER MODULE (Φ applied per module, not once over the
2815
+ // ── ONE USD LAYER PER MODULE (Φ applied per module, not once over the
2037
2816
  // composed fold) — the layered emission `usd_layers.ts` proves flattens back
2038
2817
  // to the EXACT canonical IR (asserted below, fail-closed). Per-module decls
2039
2818
  // are discovered the same way as above, on each module's OWN exports; config
2040
2819
  // export-names resolve on whichever module actually exports them; config
2041
- // `extraAggregates` ride the FIRST (weakest) layer, prelude-style. Counts /
2042
- // sums / spatials never enter the USD IR, so they have no layer placement.
2043
- if (layered) {
2820
+ // `extraAggregates` ride the FIRST (weakest) layer, prelude-style. Compiler-
2821
+ // discovered reads are seated beside the aggregate schema they read.
2822
+ {
2044
2823
  const usedStems = new Set<string>();
2045
- const inputs: LayeredModuleInput[] = mods.map((m, i) => {
2046
- let stem = path.basename(d.modules[i]!).replace(/\.[^.]*$/, "");
2824
+ let inputs: LayeredModuleInput[] = mods.map((m, i) => {
2825
+ const authoredPath = d.modules[i];
2826
+ let stem = authoredPath === undefined
2827
+ ? `nomos-derived-${i + 1}`
2828
+ : path.basename(authoredPath).replace(/\.[^.]*$/, "");
2047
2829
  for (let n = 2; usedStems.has(stem); n++) stem = `${stem}-${n}`;
2048
2830
  usedStems.add(stem);
2049
2831
 
@@ -2061,20 +2843,90 @@ async function main(): Promise<void> {
2061
2843
  ...discover<CombinedDecl>(m, isCombinedDecl),
2062
2844
  ...resolveNamed<CombinedDecl>(m, presentNames(d.combineds), "combineds"),
2063
2845
  ];
2064
- const byId = new Map<string, AggregateHandle>();
2065
- for (const a of [...aggregatesOf(m), ...(i === 0 ? extraAggregates : [])]) byId.set(a.id, a);
2066
-
2067
- const module: DomainModule = {
2846
+ // Lower the module through the SAME compiler-owned expansion seam as the
2847
+ // canonical fold. A hand-built `{aggregates,directives}` approximation silently
2848
+ // dropped relation, invariant, attestation, flow and projection opinions, so the
2849
+ // pretty USD stack was not the law. Single-module composition retains those
2850
+ // generated opinions in the layer where their authored cause lives.
2851
+ const module = composeDomainModule({
2068
2852
  name: d.name ?? d.key,
2069
2853
  domain: d.key,
2070
- aggregates: [...byId.values()],
2071
- directives: directivesOf(m),
2854
+ modules: [m],
2855
+ ...(i === 0 && extraAggregates.length > 0 ? { extraAggregates } : {}),
2856
+ // Compiler-synthesized/domain-wide declarations have no honest authored
2857
+ // module owner. Seat them once in the weakest prelude layer so USD and
2858
+ // Nomos both see one contribution instead of duplicating them per source file.
2859
+ ...(i === 0 && (composed.workspaceInvariants?.length ?? 0) > 0
2860
+ ? { workspaceInvariants: composed.workspaceInvariants }
2861
+ : {}),
2862
+ ...(i === 0 && workspaceTypes.length > 0 ? { workspaceTypes } : {}),
2863
+ ...(i === 0 && impureCapabilities.length > 0 ? { impureCapabilities } : {}),
2072
2864
  ...(layerQueries.length > 0 ? { queries: layerQueries } : {}),
2073
2865
  ...(layerDeriveds.length > 0 ? { deriveds: layerDeriveds } : {}),
2074
2866
  ...(layerCombineds.length > 0 ? { combineds: layerCombineds } : {}),
2075
- };
2867
+ });
2076
2868
  return { name: stem, module };
2077
2869
  });
2870
+ if (inputs[0] !== undefined) {
2871
+ // Work on shallow module copies: package-owned declarations usually belong
2872
+ // in the weakest prelude, but a read recipe belongs beside the aggregate
2873
+ // schema it reads. Putting a compiler-added query in an unrelated prelude
2874
+ // makes that individual USD layer ill-typed even though the whole stack has
2875
+ // the target in a stronger layer.
2876
+ inputs = inputs.map((input) => ({
2877
+ ...input,
2878
+ module: { ...input.module } as DomainModule,
2879
+ }));
2880
+ // Config-named and compiler-derived reads are package opinions rather than
2881
+ // exports of one source file. Seat only declarations missing from every
2882
+ // authored layer, once. Reads sit beside their returned aggregate; all other
2883
+ // domain-wide declarations sit in the weakest prelude.
2884
+ for (const field of [
2885
+ "queries",
2886
+ "counts",
2887
+ "spatials",
2888
+ "sums",
2889
+ "mins",
2890
+ "maxes",
2891
+ "deriveds",
2892
+ "combineds",
2893
+ "workspaceTypes",
2894
+ "workspaceInvariants",
2895
+ "orderedReads",
2896
+ "exists_",
2897
+ ] as const) {
2898
+ const canonical = (composed as unknown as Record<string, unknown>)[field];
2899
+ if (!Array.isArray(canonical)) continue;
2900
+ const present = new Set(inputs.flatMap((input) => {
2901
+ const values = (input.module as unknown as Record<string, unknown>)[field];
2902
+ return Array.isArray(values)
2903
+ ? values.flatMap((value) => value !== null && typeof value === "object" && typeof (value as { id?: unknown }).id === "string"
2904
+ ? [(value as { id: string }).id]
2905
+ : [])
2906
+ : [];
2907
+ }));
2908
+ const missing = canonical.filter((value) =>
2909
+ value !== null && typeof value === "object" && typeof (value as { id?: unknown }).id === "string"
2910
+ ? !present.has((value as { id: string }).id)
2911
+ : true);
2912
+ for (const value of missing) {
2913
+ const declarationTarget = value !== null && typeof value === "object"
2914
+ ? field === "queries"
2915
+ ? (value as { returns?: unknown }).returns
2916
+ : (["counts", "spatials", "sums", "mins", "maxes", "deriveds", "combineds"] as const).includes(field as never)
2917
+ ? (value as { of?: unknown }).of
2918
+ : undefined
2919
+ : undefined;
2920
+ const ownerIndex = typeof declarationTarget === "string"
2921
+ ? inputs.findIndex((input) => input.module.aggregates.some((aggregate) => aggregate.id === declarationTarget))
2922
+ : -1;
2923
+ const targetIndex = ownerIndex >= 0 ? ownerIndex : 0;
2924
+ const target = inputs[targetIndex]!.module as DomainModule & Record<string, unknown>;
2925
+ const existing = target[field];
2926
+ target[field] = [...(Array.isArray(existing) ? existing : []), value];
2927
+ }
2928
+ }
2929
+ }
2078
2930
  layeredDomains.push({ key: d.key, inputs });
2079
2931
  }
2080
2932
  }
@@ -2082,7 +2934,7 @@ async function main(): Promise<void> {
2082
2934
  // Presentation is a separate concern from executable law. The private application/home domain remains
2083
2935
  // in `domainModules` for hashing, routing, self-description, replay and static checking, but generated
2084
2936
  // tenant clients/proofs/interfaces contain business vocabulary only.
2085
- const presentationDomainModules = domainModules.filter(
2937
+ const presentationDomainModules = closeComposedBusinessBirthPresentation(domainModules).filter(
2086
2938
  (mod) => (mod.domain ?? mod.name) !== APPLICATION_HOME_DOMAIN,
2087
2939
  );
2088
2940
 
@@ -2248,16 +3100,10 @@ async function main(): Promise<void> {
2248
3100
  if (!rel || !exportName) fail(`report '${reportId}' must be "modulePath#exportName"; got '${ref}'`);
2249
3101
  const abs = path.resolve(cfgDir, rel);
2250
3102
  if (!existsSync(abs)) fail(`report '${reportId}': module not found: ${abs}`);
2251
- if (eagerLump) {
2252
- const v = `r${importSeq++}`;
2253
- entryImports.push(`import * as ${v} from ${JSON.stringify(abs)};`);
2254
- entryReports.push(` ${JSON.stringify(reportId)}: ${v}.${exportName},`);
2255
- } else {
2256
- // A lazy report factory: the module's `__esm` init runs on first render call.
2257
- entryReports.push(
2258
- ` ${JSON.stringify(reportId)}: (actor: string) => (require(${JSON.stringify(abs)}) as any).${exportName}(actor),`,
2259
- );
2260
- }
3103
+ // A lazy report factory: the module's init runs only when this report asset is selected.
3104
+ entryReports.push(
3105
+ ` ${JSON.stringify(reportId)}: (actor: string) => (require(${JSON.stringify(abs)}) as any).${exportName}(actor),`,
3106
+ );
2261
3107
  }
2262
3108
 
2263
3109
  // ── 1. generate + bundle the engine entry ──
@@ -2281,7 +3127,7 @@ async function main(): Promise<void> {
2281
3127
  // entry's top level — pre-freeze, exactly where the eager lump ran them.
2282
3128
  const zodSeedImports: string[] = [];
2283
3129
  const zodSeedNames: string[] = [];
2284
- if (!eagerLump) {
3130
+ {
2285
3131
  const zodPkgJson = [cfgDir, DSL_DIR]
2286
3132
  .map((dir) => {
2287
3133
  try {
@@ -2308,43 +3154,6 @@ async function main(): Promise<void> {
2308
3154
  zodSeedNames.push(v);
2309
3155
  }
2310
3156
  }
2311
- const entrySource = [
2312
- `// AUTO-GENERATED by nomos-compile from ${path.basename(configPath)} — do not edit.`,
2313
- ...(eagerLump
2314
- ? []
2315
- : [
2316
- `// LAZY PER-DOMAIN BOOT (#34): each domain registers as require-thunks; esbuild`,
2317
- `// defers every module's init to its first dispatch touch inside the fresh sandbox.`,
2318
- `declare function require(id: string): Record<string, unknown>;`,
2319
- ...zodSeedImports,
2320
- ]),
2321
- ...entryImports,
2322
- `import { registerEngine } from "@githolon/dsl/engine-entry";`,
2323
- ``,
2324
- `registerEngine({`,
2325
- ` domains: {`,
2326
- ...entryDomains,
2327
- ` },`,
2328
- ...(entryReports.length > 0 ? [` reports: {`, ...entryReports, ` },`] : []),
2329
- ...(eagerLump ? [] : [` routing: ${JSON.stringify(routing)},`]),
2330
- ...(zodSeedNames.length > 0 ? [` seeds: [${zodSeedNames.join(", ")}],`] : []),
2331
- // THE SELF-DESCRIBE INPUTS (G1): the finished read/maintenance decls + the package title, baked
2332
- // into the lump so `globalThis.describe` reproduces this compile's `<name>.openapi.json` over the
2333
- // FOLDED LAW — the kernel serves the spec, the host stages nothing. Omit-when-empty (decl-free
2334
- // packages: offers still derive from directives; the title default is the workspace-level title).
2335
- ...(Object.keys(openapiDeclsByDomain).length > 0
2336
- ? [` openapiDecls: ${JSON.stringify(openapiDeclsByDomain)},`]
2337
- : []),
2338
- ` packageTitle: ${JSON.stringify(`${cfg.name} (nomos package)`)},`,
2339
- `});`,
2340
- // ROUTE-TAGGED IN-PLAN MINTS (sharding §4, slice 3): wraps globalThis.plan +
2341
- // globalThis.nomos.mint at top-level eval — pre-freeze, after registerEngine's
2342
- // own assignments. Empty (and therefore byte-absent) for taxonomy-free packages.
2343
- ...entryMintInstalls,
2344
- ``,
2345
- ].join("\n");
2346
- const entryPath = path.join(outDir, ".nomos_entry.ts");
2347
- writeFileSync(entryPath, entrySource, "utf8");
2348
3157
 
2349
3158
  // Resolve esbuild through Node's resolver (caller's copy first, else the DSL's
2350
3159
  // own dependency) — NOT node_modules/.bin paths, which hoisted/workspace
@@ -2367,64 +3176,95 @@ async function main(): Promise<void> {
2367
3176
  const esbuildBin = path.join(path.dirname(esbuildPkgJson), esbuildBinRel ?? "bin/esbuild");
2368
3177
  const esbuildMod = (await import(pathToFileURL(path.join(path.dirname(esbuildPkgJson), "lib", "main.js")).href)) as typeof import("esbuild");
2369
3178
 
2370
- const canonicalizeEngineJavascript = (source: string): string => {
2371
- // esbuild includes module labels derived from the package's install topology in
2372
- // non-minified bundles. The same law therefore used to hash differently when the
2373
- // DSL was reached through this monorepo (../../dsl/src/...) and through a packed
2374
- // dependency (node_modules/@githolon/dsl/src/...). Canonicalise only esbuild's
2375
- // generated comment/function labels: tenant strings and executable imports are
2376
- // deliberately outside these narrow replacements.
2377
- const body = source
2378
- .replace(/^\s*"use strict";\s*$/gm, "")
2379
- .replace(/^(\s*\/\/ )(?:(?:\.\.\/)+dsl\/src\/|node_modules\/@githolon\/dsl\/src\/)/gm, "$1@githolon/dsl/src/")
2380
- .replace(/"(?:(?:\.\.\/)+dsl\/src\/|node_modules\/@githolon\/dsl\/src\/)([^"]+)"\(\) \{/g, '"@githolon/dsl/src/$1"() {')
2381
- .replace(/^(\s*\/\/ )(?:(?:\.\.\/)+node_modules\/zod\/|node_modules\/zod\/)/gm, "$1zod/")
2382
- .replace(/"(?:(?:\.\.\/)+node_modules\/zod\/|node_modules\/zod\/)([^"]+)"\(\) \{/g, '"zod/$1"() {')
2383
- .replace(/\n[ \t]*\n/g, "\n")
2384
- .trimEnd();
2385
- return `"use strict";\n${body}\n`;
2386
- };
2387
-
2388
3179
  const compact = compactFlag || cfg.compact === true;
2389
- let javascript: string;
2390
- if (compact) {
2391
- // COMPACT LUMP (see PackageConfig.compact): the programmatic esbuild API (same pinned
2392
- // package as the binary) so a resolve-plugin can stub zod's locale barrel to en-only —
2393
- // the CLI binary has no plugin surface. Same bundle options + minify; deterministic
2394
- // (pinned esbuild version, deterministic minifier).
2395
- const localeStripToEn = {
2396
- name: "nomos-zod-locales-en-only",
2397
- setup(build: import("esbuild").PluginBuild) {
2398
- build.onResolve({ filter: /locales\/index\.js$/ }, (args) => {
2399
- const resolved = path.resolve(args.resolveDir, args.path);
2400
- if (!/[\/\\]zod[\/\\]/.test(resolved)) return null; // only zod's own barrel
2401
- return { path: resolved, namespace: "nomos-zod-locales-en" };
2402
- });
2403
- build.onLoad({ filter: /.*/, namespace: "nomos-zod-locales-en" }, (args) => ({
2404
- // en ONLY: the default error map — messages stay byte-identical to today's default,
2405
- // so typed refusal payloads (which carry codes + en text) replay unchanged.
2406
- contents: 'export { default as en } from "./en.js";',
2407
- resolveDir: path.dirname(args.path),
2408
- loader: "js" as const,
2409
- }));
2410
- },
2411
- };
2412
- const built = await esbuildMod.build({
2413
- entryPoints: [entryPath], bundle: true, format: "iife", target: "es2020",
2414
- platform: "neutral", charset: "utf8", minify: true, write: false,
2415
- absWorkingDir: cfgDir, plugins: [localeStripToEn], logLevel: "silent",
2416
- });
2417
- javascript = built.outputFiles![0]!.text.trimEnd() + "\n";
2418
- } else {
2419
- javascript = execFileSync(
2420
- esbuildBin,
2421
- [entryPath, "--bundle", "--format=iife", "--target=es2020", "--platform=neutral", "--charset=utf8"],
2422
- { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], cwd: cfgDir, maxBuffer: 256 * 1024 * 1024 },
2423
- ).trimEnd() + "\n";
2424
- }
2425
- javascript = canonicalizeEngineJavascript(javascript);
2426
- if (!/globalThis\.plan\s*=/.test(javascript) && !/globalThis\["plan"\]\s*=/.test(javascript)) {
2427
- fail(`bundled engine entry does not assign globalThis.plan — refusing to package`);
3180
+
3181
+ // ── CONTENT-ADDRESSED PLAN ASSETS ─────────────────────────────────────────────
3182
+ // The ledger package no longer executes the package-wide compatibility lump above. Build the
3183
+ // smallest domain set each dispatch class needs, index it by a typed selector, and let the kernel
3184
+ // cache/pool the selected plan by its own content hash. Identical domain sets compile once and are
3185
+ // shared by every selector that needs them.
3186
+ const planAssets: UsdPlanAsset[] = [];
3187
+ const planRoutes: UsdPlanRoute[] = [];
3188
+ const planBySignature = new Map<string, UsdPlanAsset>();
3189
+ let planSequence = 0;
3190
+ const orderedDomainKeys = [...entryDomainSourceByKey.keys()];
3191
+ const planEntrySource = (domainKeys: readonly string[], includeReports: boolean): string => {
3192
+ const wanted = new Set(domainKeys);
3193
+ const decls = Object.fromEntries(Object.entries(openapiDeclsByDomain).filter(([key]) => wanted.has(key)));
3194
+ return [
3195
+ `// AUTO-GENERATED Nomos plan asset from ${path.basename(configPath)} — do not edit.`,
3196
+ `declare function require(id: string): Record<string, unknown>;`,
3197
+ ...zodSeedImports,
3198
+ `import { registerEngine } from "@githolon/dsl/engine-entry";`,
3199
+ ``,
3200
+ `registerEngine({`,
3201
+ ` domains: {`,
3202
+ ...domainKeys.map((key) => {
3203
+ const source = entryDomainSourceByKey.get(key);
3204
+ if (source === undefined) throw new Error(`missing generated entry source for '${key}'`);
3205
+ return source;
3206
+ }),
3207
+ ` },`,
3208
+ ...(includeReports && entryReports.length > 0 ? [` reports: {`, ...entryReports, ` },`] : []),
3209
+ ` routing: ${JSON.stringify(routing)},`,
3210
+ ...(zodSeedNames.length > 0 ? [` seeds: [${zodSeedNames.join(", ")}],`] : []),
3211
+ ...(Object.keys(decls).length > 0 ? [` openapiDecls: ${JSON.stringify(decls)},`] : []),
3212
+ ` packageTitle: ${JSON.stringify(`${cfg.name} (nomos package)`)},`,
3213
+ `});`,
3214
+ ...domainKeys.flatMap((key) => {
3215
+ const install = entryMintInstallByKey.get(key);
3216
+ return install === undefined ? [] : [install];
3217
+ }),
3218
+ ``,
3219
+ ].join("\n");
3220
+ };
3221
+ const compilePlan = (domainKeys: readonly string[], includeReports: boolean): UsdPlanAsset => {
3222
+ const ordered = orderedDomainKeys.filter((key) => domainKeys.includes(key));
3223
+ const signature = JSON.stringify({ domains: ordered, reports: includeReports });
3224
+ const hit = planBySignature.get(signature);
3225
+ if (hit !== undefined) return hit;
3226
+ const sourcePath = path.join(outDir, `.nomos_plan_${planSequence++}.ts`);
3227
+ writeFileSync(sourcePath, planEntrySource(ordered, includeReports), "utf8");
3228
+ const args = [
3229
+ sourcePath,
3230
+ "--bundle", "--format=iife", "--target=es2020", "--platform=neutral", "--charset=utf8",
3231
+ "--define:__nmsRuntimeLaw=true",
3232
+ ...(compact ? ["--minify"] : []),
3233
+ ];
3234
+ let bundled = execFileSync(esbuildBin, args, {
3235
+ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], cwd: cfgDir, maxBuffer: 256 * 1024 * 1024,
3236
+ }).trimEnd() + "\n";
3237
+ bundled = canonicalizeEngineJavascript(bundled);
3238
+ if (!/globalThis\.plan\s*=/.test(bundled) && !/globalThis\["plan"\]\s*=/.test(bundled)) {
3239
+ fail(`generated plan asset does not assign globalThis.plan — refusing to package`);
3240
+ }
3241
+ const asset = { sha256: sha256HexUtf8(bundled), javascript: bundled };
3242
+ planBySignature.set(signature, asset);
3243
+ planAssets.push(asset);
3244
+ return asset;
3245
+ };
3246
+ const routePlan = (selector: string, domainKeys: readonly string[], includeReports = false): void => {
3247
+ const asset = compilePlan(domainKeys, includeReports);
3248
+ planRoutes.push({ selector, planSha256: asset.sha256 });
3249
+ };
3250
+ for (const key of orderedDomainKeys) routePlan(`plan:domain:${key}`, [key]);
3251
+ routePlan("describe", orderedDomainKeys);
3252
+ for (const reportId of Object.keys(cfg.reports ?? {})) routePlan(`report:report:${reportId}`, [], true);
3253
+ const routeTable = (
3254
+ prefix: string,
3255
+ table: Readonly<Record<string, readonly string[]>> | undefined,
3256
+ ): void => {
3257
+ for (const [id, domains] of Object.entries(table ?? {})) routePlan(`${prefix}${id}`, domains);
3258
+ };
3259
+ routeTable("report:derive:", routing.derivedOf);
3260
+ routeTable("report:combine:", routing.combinedOf);
3261
+ routeTable("report:aggregateInvariant:", routing.aggregateInvariantOf);
3262
+ routeTable("report:invariant:", routing.relationOf);
3263
+ routeTable("report:workspaceInvariantReads:", routing.workspaceInvariantOf);
3264
+ routeTable("report:workspaceInvariant:", routing.workspaceInvariantOf);
3265
+ const workspaceInvariantDomains = [...new Set(Object.values(routing.workspaceInvariantOf ?? {}).flat())];
3266
+ if (workspaceInvariantDomains.length > 0) {
3267
+ routePlan("report:workspaceInvariantList", workspaceInvariantDomains);
2428
3268
  }
2429
3269
 
2430
3270
  // ── 1a. THE CAPTURED-READ GATE (#57, lifted-when-declared since #58) ──
@@ -2441,14 +3281,18 @@ async function main(): Promise<void> {
2441
3281
  // Slice B: a FOREIGN (attested) read declaration ALSO opts the law into the read lane —
2442
3282
  // `read(q, args, {from})` is served from the verified injected envelope, so a law whose
2443
3283
  // only reads are foreign is just as declared as a local-read law.
2444
- const declaresCapturedReads = domainModules.some((m) =>
3284
+ // Framework-private authority law has its own declared read. It must not globally bless an undeclared
3285
+ // tenant `read()` merely because both happen to share one package lump. Only presented business law can
3286
+ // satisfy the tenant captured-read declaration gate; the private law uses `readFrameworkDeclared` and
3287
+ // deliberately does not carry the tenant marker.
3288
+ const declaresCapturedReads = presentationDomainModules.some((m) =>
2445
3289
  m.directives.some(
2446
3290
  (d) =>
2447
3291
  ((d as { declaredQueryReads?: unknown[] }).declaredQueryReads ?? []).length > 0 ||
2448
3292
  ((d as { declaredForeignReads?: unknown[] }).declaredForeignReads ?? []).length > 0,
2449
3293
  ),
2450
3294
  );
2451
- if (javascript.includes(CAPTURED_READ_LUMP_MARKER) && !declaresCapturedReads) {
3295
+ if (planAssets.some((asset) => asset.javascript.includes(CAPTURED_READ_LUMP_MARKER)) && !declaresCapturedReads) {
2452
3296
  fail(
2453
3297
  `this law references read() — the CAPTURED-READ lane — but NO directive declares a ` +
2454
3298
  `captured-read query, so the sealed engine would provide no nomos.read and every ` +
@@ -2480,62 +3324,67 @@ async function main(): Promise<void> {
2480
3324
  }
2481
3325
  }
2482
3326
 
2483
- // ── 1b. THE FROZEN-SANDBOX BOOT PROBE (lazy boot only, fail-closed) ──
2484
- // The sealed engine freezes globalThis after the lump's top level; a lazy
3327
+ // ── 1b. THE FROZEN-SANDBOX BOOT PROBE (fail-closed) ──
3328
+ // The sealed engine freezes globalThis after a plan asset's top level; a lazy
2485
3329
  // domain's module init runs LATER, inside the dispatch. Any module that writes
2486
3330
  // an ambient global at init (the zod slots are pre-seeded above; this guards
2487
3331
  // everything else) would deterministically throw at FIRST DISPATCH in
2488
3332
  // production. Reproduce the sandbox order here — eval the real bundle in a
2489
3333
  // fresh frozen context, then force every domain — and refuse to package on any
2490
3334
  // failure that is not the expected unknown-directive refusal.
2491
- if (!eagerLump) {
3335
+ {
2492
3336
  // A CHILD process: the probe must freeze a REAL global object (a `vm` context
2493
3337
  // global refuses Object.freeze), and the lump must run STRICT (the engine
2494
3338
  // evals it with the STRICT flag — sloppy mode would silently no-op the very
2495
3339
  // frozen-global writes the probe exists to catch).
2496
- const lumpPath = path.join(outDir, ".nomos_lump.js");
2497
- writeFileSync(lumpPath, javascript, "utf8");
2498
3340
  const probePath = path.join(outDir, ".nomos_boot_probe.cjs");
2499
3341
  writeFileSync(
2500
3342
  probePath,
2501
3343
  [
2502
3344
  `// AUTO-GENERATED by nomos-compile — the frozen-sandbox lazy-boot probe.`,
2503
3345
  `const fs = require("node:fs");`,
2504
- `const lump = fs.readFileSync(process.argv[2], "utf8");`,
2505
- `const domains = JSON.parse(process.argv[3]);`,
2506
- `(0, eval)('"use strict";\\n' + lump); // indirect eval → real global scope, strict like the engine`,
3346
+ `const plan = fs.readFileSync(process.argv[2], "utf8");`,
3347
+ `const key = process.argv[3];`,
3348
+ `(0, eval)('"use strict";\\n' + plan); // indirect eval → real global scope, strict like the engine`,
2507
3349
  `Object.freeze(globalThis); // the sealed sandbox freezes before dispatch`,
2508
- `for (const key of domains) {`,
2509
- ` let msg;`,
2510
- ` try { globalThis.plan({ intent: { domain: key, directiveId: "__nomos_boot_probe__" } }); msg = "unexpected-success"; }`,
2511
- ` catch (e) { msg = String((e && e.message) || e); }`,
2512
- ` if (!/no directive registered/.test(msg)) { console.error(key + "\\u0000" + msg); process.exit(3); }`,
2513
- `}`,
3350
+ `let msg;`,
3351
+ `try { globalThis.plan({ intent: { domain: key, directiveId: "__nomos_boot_probe__" } }); msg = "unexpected-success"; }`,
3352
+ `catch (e) { msg = String((e && e.message) || e); }`,
3353
+ `if (!/no directive registered/.test(msg)) { console.error(key + "\\u0000" + msg); process.exit(3); }`,
2514
3354
  ``,
2515
3355
  ].join("\n"),
2516
3356
  "utf8",
2517
3357
  );
2518
- try {
2519
- execFileSync(process.execPath, [probePath, lumpPath, JSON.stringify(domainsToCompile.map((d) => d.key))], {
2520
- encoding: "utf8",
2521
- stdio: ["ignore", "ignore", "pipe"],
2522
- });
2523
- } catch (e) {
2524
- const stderr = String((e as { stderr?: string }).stderr ?? "").trim();
2525
- const [domainKey, msg] = stderr.includes("\u0000") ? stderr.split("\u0000") : ["?", stderr];
2526
- fail(
2527
- `domain '${domainKey}' failed its frozen-sandbox lazy-boot probe — a module ` +
2528
- `initializer touches ambient globals after the freeze (the engine would ` +
2529
- `refuse its first dispatch): ${msg}`,
2530
- );
3358
+ for (const route of planRoutes.filter((candidate) => candidate.selector.startsWith("plan:domain:"))) {
3359
+ const domainKey = route.selector.slice("plan:domain:".length);
3360
+ const asset = planAssets.find((candidate) => candidate.sha256 === route.planSha256)!;
3361
+ const planPath = path.join(outDir, `.nomos_boot_${planSequence++}.js`);
3362
+ writeFileSync(planPath, asset.javascript, "utf8");
3363
+ try {
3364
+ execFileSync(process.execPath, [probePath, planPath, domainKey], {
3365
+ encoding: "utf8", stdio: ["ignore", "ignore", "pipe"],
3366
+ });
3367
+ } catch (e) {
3368
+ const stderr = String((e as { stderr?: string }).stderr ?? "").trim();
3369
+ const [, msg = stderr] = stderr.includes("\u0000") ? stderr.split("\u0000") : ["?", stderr];
3370
+ fail(
3371
+ `domain '${domainKey}' failed its frozen-sandbox plan probe — a module initializer ` +
3372
+ `touches ambient globals after the freeze (the engine would refuse its first dispatch): ${msg}`,
3373
+ );
3374
+ }
2531
3375
  }
2532
3376
  }
2533
3377
 
2534
3378
  // ── 2. USD IR + the package envelope ──
2535
3379
  const usdJson = emitUsdJsonForModules(domainModules);
2536
- const text = packageUsda(usdJson, javascript, { compact });
3380
+ const text = nativePlanPackageUsda(
3381
+ cfg.name,
3382
+ JSON.parse(usdJson),
3383
+ domainModules,
3384
+ planAssets,
3385
+ planRoutes,
3386
+ );
2537
3387
  const pkgPath = path.join(outDir, `${cfg.name}.package.usda`);
2538
- writeFileSync(pkgPath, text, "utf8");
2539
3388
  const domainHash = sha256HexUtf8(text);
2540
3389
 
2541
3390
  // ── 2a. the REAL layered RELATION `.usda` set — an additive, deterministic visualization +
@@ -2563,22 +3412,54 @@ async function main(): Promise<void> {
2563
3412
  }
2564
3413
  }
2565
3414
 
2566
- // ── 2b. opt-in LAYERED emission — additive artifacts ONLY (the flattened package
2567
- // above and its domainHash are already written and do not move). The
2568
- // homomorphism is ASSERTED fail-closed on every layered compile: the
2569
- // per-module layer stack must flatten BYTE-IDENTICALLY to the canonical
2570
- // composed IR embedded in the package, or nothing layered is emitted.
3415
+ // ── 2b. REQUIRED LAYERED emission — the executable envelope above retains the
3416
+ // canonical resolved law, while these are the authored/composable source layers. The
3417
+ // homomorphism is ASSERTED fail-closed on every compile: the
3418
+ // per-module layer stack must resolve to the same canonical law embedded
3419
+ // in the package, or compilation refuses.
2571
3420
  let layeredRootPath: string | undefined;
2572
3421
  let layeredFileCount = 0;
2573
- if (layered) {
3422
+ let usdcatFlattenVerified = false;
3423
+ let composedUsdIdentity: string | undefined;
3424
+ {
2574
3425
  const safe = (s: string) => s.replace(/[^A-Za-z0-9._-]/g, "_");
2575
3426
  const layersDir = path.join(outDir, "layers");
2576
3427
  mkdirSync(layersDir, { recursive: true });
2577
3428
  const allLayers: UsdLayer[] = [];
2578
3429
  const relPaths: string[] = [];
2579
3430
  const pendingWrites: { path: string; text: string }[] = [];
3431
+ const canonicalLayers = new Map(
3432
+ (JSON.parse(usdJson) as { layers: UsdLayer[] }).layers.map((layer) => [layer.path, layer] as const),
3433
+ );
2580
3434
  for (const ld of layeredDomains) {
2581
- for (const emitted of emitLayeredDomain(ld.key, ld.inputs)) {
3435
+ const authored = emitLayeredDomain(ld.key, ld.inputs, { applyBusinessCorrectionLayer: false });
3436
+ const canonicalLayer = canonicalLayers.get(`/Nomos/${ld.key}`);
3437
+ // Workspace invariants include compiler-owned consequences of the complete
3438
+ // application language (birth quota, receiving-group policy, recovery). They
3439
+ // are not owned by one source file, so seat the canonical declaration once in
3440
+ // the weakest package prelude instead of losing it or duplicating it. Contract
3441
+ // implementation links are likewise a cross-module fact: the contract may be
3442
+ // declared in one layer and implemented by an aggregate in another.
3443
+ const emittedLayers = authored.map((emitted, index) => {
3444
+ if (index !== 0 || canonicalLayer === undefined) return emitted;
3445
+ const needsPackagePrelude = canonicalLayer.nomosInvariantGate !== undefined || canonicalLayer.nomosContracts !== undefined;
3446
+ if (!needsPackagePrelude) return emitted;
3447
+ const layer: UsdLayer = {
3448
+ ...emitted.layer,
3449
+ ...(canonicalLayer.nomosInvariantGate !== undefined
3450
+ ? { nomosInvariantGate: canonicalLayer.nomosInvariantGate }
3451
+ : {}),
3452
+ ...(canonicalLayer.nomosContracts !== undefined
3453
+ ? { nomosContracts: canonicalLayer.nomosContracts }
3454
+ : {}),
3455
+ };
3456
+ return {
3457
+ ...emitted,
3458
+ layer,
3459
+ usda: usdaLayerText(ld.key, emitted.name, layer, ld.inputs[index]!.module, new Set()),
3460
+ };
3461
+ });
3462
+ for (const emitted of emittedLayers) {
2582
3463
  const base = `${safe(ld.key)}--${safe(emitted.name)}`;
2583
3464
  pendingWrites.push({ path: path.join(layersDir, `${base}.usda`), text: emitted.usda });
2584
3465
  pendingWrites.push({
@@ -2590,20 +3471,89 @@ async function main(): Promise<void> {
2590
3471
  }
2591
3472
  }
2592
3473
  const flat = flattenLayeredUsd(allLayers);
2593
- const flatJson = JSON.stringify(flat);
2594
- if (flatJson !== usdJson) {
3474
+ composedUsdIdentity = sha256HexUtf8(canonicalJson(flat));
3475
+ const canonicalLaw = JSON.parse(usdJson);
3476
+ if (canonicalJson(flat) !== canonicalJson(canonicalLaw)) {
3477
+ const difference = firstLawDifference(canonicalLaw, flat) ?? "canonical law differs";
2595
3478
  fail(
2596
3479
  `layered emission does not flatten to the canonical composed IR — the module fold and ` +
2597
3480
  `the layer-stack fold DISAGREE for this package (usually a re-declared directive ` +
2598
3481
  `silently dropping an omit-when-empty reads/emits boundary, or a re-declared ` +
2599
3482
  `aggregate dropping a field: USD shows the weaker opinion through; the module fold ` +
2600
- `does not). Refusing to emit the layered artifact (the flattened package is unaffected).`,
3483
+ `does not). First difference: ${difference}. Refusing to emit the package.`,
2601
3484
  );
2602
3485
  }
2603
3486
  for (const w of pendingWrites) writeFileSync(w.path, w.text, "utf8");
2604
3487
  layeredRootPath = path.join(outDir, `${cfg.name}.layered.usda`);
2605
3488
  writeFileSync(layeredRootPath, layeredRootUsda(relPaths, flat), "utf8");
2606
3489
  layeredFileCount = relPaths.length;
3490
+
3491
+ const usdcat = resolveUsdcat();
3492
+ if (usdcat === undefined) {
3493
+ if (requireUsdcat) {
3494
+ fail(
3495
+ "stock OpenUSD `usdcat` is required for this compile but was not found. " +
3496
+ "Install OpenUSD or set NOMOS_USDCAT to the executable path.",
3497
+ );
3498
+ }
3499
+ } else {
3500
+ let flattenedText: string;
3501
+ try {
3502
+ flattenedText = execFileSync(usdcat, ["--flatten", layeredRootPath], {
3503
+ encoding: "utf8",
3504
+ stdio: ["ignore", "pipe", "pipe"],
3505
+ maxBuffer: 256 * 1024 * 1024,
3506
+ });
3507
+ } catch (error) {
3508
+ const stderr = String((error as { stderr?: string }).stderr ?? "").trim();
3509
+ fail(`stock OpenUSD refused the generated layer stack: ${stderr || String(error)}`);
3510
+ }
3511
+ const openUsdLaw = parseUsdaDocument(flattenedText);
3512
+ if (canonicalJson(openUsdLaw) !== canonicalJson(canonicalLaw)) {
3513
+ const difference = firstLawDifference(canonicalLaw, openUsdLaw) ?? "canonical law differs";
3514
+ fail(
3515
+ `stock OpenUSD and Nomos resolve different effective law. First difference: ${difference}. ` +
3516
+ `Refusing to emit the package.`,
3517
+ );
3518
+ }
3519
+ usdcatFlattenVerified = true;
3520
+ }
3521
+ }
3522
+ // The executable package is the COMMITTED result of composition. Do not leave
3523
+ // a newly-written package behind when either Nomos or stock OpenUSD rejects its
3524
+ // source stack; rejected layer artifacts remain useful diagnostics, not runnable law.
3525
+ writeFileSync(pkgPath, text, "utf8");
3526
+
3527
+ // ── 2c. THE PUBLISHED OPENUSD PACKAGE ASSET ──
3528
+ // `rootPart` is the explicit package/application boundary from the OpenUSD composition ADR.
3529
+ // The asset references the REAL authored layer stack. It deliberately contains neither the
3530
+ // legacy executable lump nor a hidden JSON copy of the law: this is the source of truth the
3531
+ // application composer consumes, and the shape the ledger/runtime migration targets.
3532
+ let packageAssetPath: string | undefined;
3533
+ if (cfg.rootPart !== undefined) {
3534
+ try {
3535
+ const packageAsset = packageAssetUsda({
3536
+ packageName: cfg.name,
3537
+ packageIdentity: composedUsdIdentity!,
3538
+ domains: presentationDomainModules.map((module) => module.domain ?? module.name),
3539
+ root: cfg.rootPart,
3540
+ lawAsset: `./${cfg.name}.layered.usda`,
3541
+ });
3542
+ packageAssetPath = path.join(outDir, `${cfg.name}.asset.usda`);
3543
+ const candidateAssetPath = path.join(outDir, `.${cfg.name}.asset.candidate.usda`);
3544
+ writeFileSync(candidateAssetPath, packageAsset, "utf8");
3545
+ const usdcat = resolveUsdcat();
3546
+ if (usdcat !== undefined) {
3547
+ execFileSync(usdcat, ["--loadOnly", candidateAssetPath], {
3548
+ encoding: "utf8",
3549
+ stdio: ["ignore", "ignore", "pipe"],
3550
+ });
3551
+ }
3552
+ renameSync(candidateAssetPath, packageAssetPath);
3553
+ } catch (error) {
3554
+ rmSync(path.join(outDir, `.${cfg.name}.asset.candidate.usda`), { force: true });
3555
+ fail(`published OpenUSD package asset refused: ${(error as Error).message}`);
3556
+ }
2607
3557
  }
2608
3558
 
2609
3559
  // ── 3. the IDENTITY emit (for the summary's per-domain identityHash) ──
@@ -2667,7 +3617,7 @@ async function main(): Promise<void> {
2667
3617
  // (currentLaw); the host ingests it and builds its router from it — no domain/directive name in host
2668
3618
  // source. Self-describing for the whole web ecosystem (Swagger/codegen/Hono/agent tool-calling).
2669
3619
  const publicOpenapi = mergeOpenApiFragments(
2670
- domainModules.map((m) => generateOpenApiFragment(m, { lawHash: domainHash })),
3620
+ presentationDomainModules.map((m) => generateOpenApiFragment(m, { lawHash: domainHash })),
2671
3621
  { title: `${cfg.name} (nomos package)`, lawHash: domainHash },
2672
3622
  );
2673
3623
  const runtimeOpenapi = mergeOpenApiFragments(
@@ -2743,6 +3693,10 @@ async function main(): Promise<void> {
2743
3693
  }
2744
3694
  } catch (e) {
2745
3695
  proofSkip = (e as Error).message;
3696
+ // A skipped generation must not leave yesterday's client story in build/. Otherwise editors and CI
3697
+ // typecheck a stale proof which no longer describes the compiled law — worse than having no proof.
3698
+ rmSync(path.join(outDir, `${cfg.name}.proof.mts`), { force: true });
3699
+ rmSync(path.join(outDir, `${cfg.name}.proof-legs.json`), { force: true });
2746
3700
  }
2747
3701
  // BIRTHS DX: a `.births()` directive that can't be (or can only shakily be) proven says WHY here —
2748
3702
  // never a silent skip, never an opaque runtime `born=[]` first contact. When the .mts generation
@@ -2767,7 +3721,10 @@ async function main(): Promise<void> {
2767
3721
  ` queries: ${d.queries.join(", ") || "—"} counts: ${d.counts.join(", ") || "—"} sums: ${d.sums.join(", ") || "—"}`,
2768
3722
  ]),
2769
3723
  `deploy: POST ${cfg.name}.deploy.json to /v2/workspaces/<ws>/domains (or: githolon deploy <ws>)`,
2770
- `typed client: ${cfg.name}.client.ts — ${tsClientFactoryName(presentationDomainModules[0]?.domain ?? presentationDomainModules[0]?.name ?? cfg.name)}(holon), law hash baked in`,
3724
+ packageAssetPath !== undefined
3725
+ ? `package asset: ${path.basename(packageAssetPath)} — the real USD package root consumed by application composition`
3726
+ : `package asset: NOT EMITTED — declare rootPart: { name, kind: "component" | "assembly" }`,
3727
+ `typed application: ${cfg.name}.client.mjs — open${cfg.name.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => part[0]!.toUpperCase() + part.slice(1)).join("")}Application({ cloud, signIn })`,
2771
3728
  proofPath !== undefined
2772
3729
  ? `generated proof: ${cfg.name}.proof.mts — run: githolon proof (live; ends ALL GREEN or names the jam)`
2773
3730
  : `generated proof: SKIPPED — ${proofSkip}`,
@@ -2777,14 +3734,257 @@ async function main(): Promise<void> {
2777
3734
 
2778
3735
  // ── 5. the typed TS client (#10 — the web sibling of codegen_dart), with the
2779
3736
  // deployed law's content hash baked in (emitted AFTER the package is hashed) ──
2780
- const clientPath = path.join(outDir, `${cfg.name}.client.ts`);
2781
- const clientSource = generateTsClient(presentationDomainModules, { packageName: cfg.name, domainHash, interface: packageInterface, compat: nomosCompat });
3737
+ const clientPath = path.join(outDir, `${cfg.name}.client.mjs`);
3738
+ const clientDeclarationPath = path.join(outDir, `${cfg.name}.client.d.mts`);
3739
+ const clientSource = generateTsClient(presentationDomainModules, {
3740
+ packageName: cfg.name,
3741
+ domainHash,
3742
+ interface: packageInterface,
3743
+ compat: nomosCompat,
3744
+ homeDomain: resolveApplicationTopology(presentationDomainModules).homeDomain,
3745
+ });
3746
+ const runtimePath = path.join(DSL_DIR, "runtime", "generated-application-host.js");
3747
+ if (!existsSync(runtimePath)) {
3748
+ fail("the compiler-private generated application runtime is missing — reinstall @githolon/dsl");
3749
+ }
3750
+ const temporaryClientPath = path.join(outDir, `.${cfg.name}.client.source.ts`);
3751
+ const temporaryRuntimeTypesPath = path.join(outDir, `.${cfg.name}.application-runtime.d.ts`);
3752
+ const temporaryDeclarationPath = temporaryClientPath.replace(/\.ts$/, ".d.ts");
3753
+ writeFileSync(temporaryClientPath, clientSource, "utf8");
3754
+ writeFileSync(temporaryRuntimeTypesPath, [
3755
+ `export declare function nomosInternalOpenGeneratedApplication<T extends object>(`,
3756
+ ` options: { cloud: string; application: string; signIn: (request?: { readonly deviceChallenge?: string }) => Promise<string> },`,
3757
+ ` bind: (holon: unknown, user: { readonly identityId: string; readonly identityAddress: string }) => T,`,
3758
+ `): Promise<T & {`,
3759
+ ` currentStatus(): Promise<any>;`,
3760
+ ` watchStatus(listener: (status: any) => void): () => void;`,
3761
+ `}>;`,
3762
+ `export declare function nomosInternalOpenGeneratedChildApplication<T extends object>(`,
3763
+ ` parent: unknown,`,
3764
+ ` workspace: string,`,
3765
+ ` bind: (holon: unknown) => T,`,
3766
+ `): Promise<T>;`,
3767
+ `export declare function nomosInternalCreateInvitation(`,
3768
+ ` holon: unknown,`,
3769
+ `): Promise<{ readonly address: string; readonly token: string }>;`,
3770
+ `export declare function nomosInternalRedeemInvitation(`,
3771
+ ` holon: unknown,`,
3772
+ ` link: string,`,
3773
+ ` policies: readonly { readonly domain: string; readonly domainHash: string; readonly query: string; readonly redeem: string; readonly personField: string; readonly roleField: string; readonly scopeField: string }[],`,
3774
+ `): Promise<void>;`,
3775
+ ``,
3776
+ ].join("\n"), "utf8");
2782
3777
  try {
2783
- await esbuildMod.transform(clientSource, { loader: "ts", format: "esm", target: "es2020", logLevel: "silent" });
3778
+ const typescriptEntry = createRequire(import.meta.url).resolve("typescript/lib/typescript.js");
3779
+ const ts = (await import(pathToFileURL(typescriptEntry).href)) as typeof import("typescript");
3780
+ const program = ts.createProgram(
3781
+ [temporaryClientPath, temporaryRuntimeTypesPath],
3782
+ {
3783
+ target: ts.ScriptTarget.ES2022,
3784
+ module: ts.ModuleKind.NodeNext,
3785
+ moduleResolution: ts.ModuleResolutionKind.NodeNext,
3786
+ declaration: true,
3787
+ emitDeclarationOnly: true,
3788
+ noEmitOnError: true,
3789
+ strict: true,
3790
+ skipLibCheck: true,
3791
+ allowImportingTsExtensions: true,
3792
+ baseUrl: outDir,
3793
+ paths: {
3794
+ "@githolon/client/_generated_application_host_7c1f5e": [temporaryRuntimeTypesPath],
3795
+ },
3796
+ outDir,
3797
+ },
3798
+ );
3799
+ const emit = program.emit();
3800
+ const diagnostics = [...ts.getPreEmitDiagnostics(program), ...emit.diagnostics];
3801
+ if (diagnostics.length > 0 || emit.emitSkipped || !existsSync(temporaryDeclarationPath)) {
3802
+ const detail = diagnostics.map((diagnostic) => {
3803
+ const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
3804
+ if (diagnostic.file === undefined || diagnostic.start === undefined) return message;
3805
+ const point = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
3806
+ return `${path.basename(diagnostic.file.fileName)}:${point.line + 1}:${point.character + 1}: ${message}`;
3807
+ }).join("\n");
3808
+ fail(`generated TypeScript application declarations are invalid — this is a Nomos codegen bug:\n${detail}`);
3809
+ }
3810
+ const sourceFile = program.getSourceFile(temporaryClientPath);
3811
+ const applicationBusinessName = `_${tsApplicationName(cfg.name)}Business`;
3812
+ const businessAlias = sourceFile?.statements.find(
3813
+ (statement): statement is import("typescript").TypeAliasDeclaration =>
3814
+ ts.isTypeAliasDeclaration(statement) && statement.name.text === applicationBusinessName,
3815
+ );
3816
+ if (sourceFile === undefined || businessAlias === undefined) {
3817
+ fail(`generated TypeScript application has no ${applicationBusinessName} type`);
3818
+ }
3819
+ const checker = program.getTypeChecker();
3820
+ const structuralAliases = sourceFile.statements.filter(
3821
+ (statement): statement is import("typescript").TypeAliasDeclaration =>
3822
+ ts.isTypeAliasDeclaration(statement) &&
3823
+ (statement.name.text === applicationBusinessName ||
3824
+ (statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true &&
3825
+ statement.getText(sourceFile).includes("_NomosPublicDomain<"))),
3826
+ );
3827
+ const expansions = new Map<string, { exported: boolean; body: string }>();
3828
+ for (const alias of structuralAliases) {
3829
+ const aliasType = checker.getTypeFromTypeNode(alias.type);
3830
+ const publicProperties = checker.getPropertiesOfType(aliasType);
3831
+ if (alias.name.text === applicationBusinessName && publicProperties.length === 0) {
3832
+ fail(`generated TypeScript application has no public business members`);
3833
+ }
3834
+ // `typeToString(theWholeAlias)` is not a public-boundary operation. For a large intersection TypeScript
3835
+ // deliberately preserves `_NomosPublicDomain<{ domain; domainHash; interface; ... }>` rather than
3836
+ // reducing Omit, which makes every private property textually reachable from the declaration pruner.
3837
+ // Ask the checker for the alias's ACTUAL properties first, then render each public member independently.
3838
+ // This applies equally to the root application and every child application returned by a business birth.
3839
+ const body = `{\n${publicProperties.map((property) => {
3840
+ const propertyType = checker.getTypeOfSymbolAtLocation(property, alias);
3841
+ const rendered = checker.typeToString(
3842
+ propertyType,
3843
+ alias,
3844
+ ts.TypeFormatFlags.NoTruncation | ts.TypeFormatFlags.InTypeAlias,
3845
+ );
3846
+ const optional = (property.flags & ts.SymbolFlags.Optional) !== 0 ? "?" : "";
3847
+ return ` readonly ${JSON.stringify(property.getName())}${optional}: ${rendered};`;
3848
+ }).join("\n")}\n}`;
3849
+ expansions.set(alias.name.text, {
3850
+ exported: alias.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true,
3851
+ body,
3852
+ });
3853
+ }
3854
+ if (!expansions.has(applicationBusinessName)) {
3855
+ fail(`generated TypeScript application has no public business members`);
3856
+ }
3857
+ let declarationSource = readFileSync(temporaryDeclarationPath, "utf8");
3858
+ const emittedDeclaration = ts.createSourceFile(
3859
+ temporaryDeclarationPath,
3860
+ declarationSource,
3861
+ ts.ScriptTarget.Latest,
3862
+ true,
3863
+ ts.ScriptKind.TS,
3864
+ );
3865
+ // A type alias may contain hundreds of semicolon-delimited object members. The previous regexp stopped
3866
+ // at the first one, so a real multi-domain application silently retained ReturnType<privateFactory>
3867
+ // dependencies and dragged raw domain hashes, binders and compatibility manifests into the tenant .d.mts.
3868
+ // Replace complete declaration nodes from the end of the file backward; the reachability pass below can
3869
+ // then discard private factories and every framework-only type they were keeping alive.
3870
+ const structuralDeclarations = emittedDeclaration.statements
3871
+ .filter((statement): statement is import("typescript").TypeAliasDeclaration =>
3872
+ ts.isTypeAliasDeclaration(statement) && expansions.has(statement.name.text))
3873
+ .sort((left, right) => right.getStart(emittedDeclaration) - left.getStart(emittedDeclaration));
3874
+ if (!structuralDeclarations.some((statement) => statement.name.text === applicationBusinessName)) {
3875
+ fail(`generated TypeScript declaration has no ${applicationBusinessName} type`);
3876
+ }
3877
+ for (const declaration of structuralDeclarations) {
3878
+ const expansion = expansions.get(declaration.name.text)!;
3879
+ declarationSource =
3880
+ declarationSource.slice(0, declaration.getStart(emittedDeclaration)) +
3881
+ `${expansion.exported ? "export " : ""}type ${declaration.name.text} = ${expansion.body};` +
3882
+ declarationSource.slice(declaration.end);
3883
+ }
3884
+
3885
+ // Declaration emit follows private aliases that appear in exported type expressions. Replace the one
3886
+ // application alias with its already-filtered structural business type, then retain only declarations
3887
+ // reachable from real exports. The resulting .d.mts cannot teach or autocomplete NomosHolon, raw offers,
3888
+ // binders or compatibility machinery because none are part of the public graph. Record coordinates remain
3889
+ // deliberately reachable through NomosRef<T>: addresses are public and portable; authority stays private.
3890
+ const declarationFile = ts.createSourceFile(
3891
+ clientDeclarationPath,
3892
+ declarationSource,
3893
+ ts.ScriptTarget.Latest,
3894
+ true,
3895
+ ts.ScriptKind.TS,
3896
+ );
3897
+ const namesOf = (statement: import("typescript").Statement): readonly string[] => {
3898
+ if (
3899
+ ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) ||
3900
+ ts.isClassDeclaration(statement) || ts.isEnumDeclaration(statement) ||
3901
+ ts.isFunctionDeclaration(statement)
3902
+ ) return statement.name === undefined ? [] : [statement.name.text];
3903
+ if (ts.isVariableStatement(statement)) {
3904
+ return statement.declarationList.declarations.flatMap((declaration) =>
3905
+ ts.isIdentifier(declaration.name) ? [declaration.name.text] : []
3906
+ );
3907
+ }
3908
+ return [];
3909
+ };
3910
+ const exported = (statement: import("typescript").Statement): boolean =>
3911
+ ts.isExportDeclaration(statement) || ts.isExportAssignment(statement) ||
3912
+ (ts.canHaveModifiers(statement) &&
3913
+ (ts.getModifiers(statement)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false));
3914
+ const declarations = new Map<string, number>();
3915
+ declarationFile.statements.forEach((statement, index) => {
3916
+ for (const name of namesOf(statement)) declarations.set(name, index);
3917
+ });
3918
+ const keep = new Set<number>();
3919
+ const queue: number[] = [];
3920
+ declarationFile.statements.forEach((statement, index) => {
3921
+ if (exported(statement)) { keep.add(index); queue.push(index); }
3922
+ });
3923
+ while (queue.length > 0) {
3924
+ const index = queue.pop()!;
3925
+ const visit = (node: import("typescript").Node): void => {
3926
+ if (ts.isIdentifier(node)) {
3927
+ const dependency = declarations.get(node.text);
3928
+ if (dependency !== undefined && !keep.has(dependency)) {
3929
+ keep.add(dependency);
3930
+ queue.push(dependency);
3931
+ }
3932
+ }
3933
+ ts.forEachChild(node, visit);
3934
+ };
3935
+ visit(declarationFile.statements[index]!);
3936
+ }
3937
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed, removeComments: false });
3938
+ declarationSource = [
3939
+ "// GENERATED by nomos-compile — the only TypeScript tenant surface. DO NOT EDIT.",
3940
+ ...declarationFile.statements
3941
+ .filter((_statement, index) => keep.has(index))
3942
+ .map((statement) => printer.printNode(ts.EmitHint.Unspecified, statement, declarationFile)),
3943
+ "",
3944
+ ].join("\n");
3945
+ writeFileSync(temporaryDeclarationPath, declarationSource, "utf8");
3946
+ renameSync(temporaryDeclarationPath, clientDeclarationPath);
3947
+ await esbuildMod.build({
3948
+ stdin: {
3949
+ contents: clientSource,
3950
+ loader: "ts",
3951
+ resolveDir: outDir,
3952
+ sourcefile: `${cfg.name}.client.ts`,
3953
+ },
3954
+ outfile: clientPath,
3955
+ bundle: true,
3956
+ packages: "bundle",
3957
+ platform: "node",
3958
+ format: "esm",
3959
+ target: "node20",
3960
+ legalComments: "none",
3961
+ logLevel: "silent",
3962
+ plugins: [{
3963
+ name: "nomos-generated-application-runtime",
3964
+ setup(build) {
3965
+ build.onResolve(
3966
+ { filter: /^@githolon\/client\/_generated_application_host_7c1f5e$/ },
3967
+ () => ({ path: runtimePath }),
3968
+ );
3969
+ },
3970
+ }],
3971
+ });
3972
+ const bundled = canonicalizeGeneratedClientJavascript(readFileSync(clientPath, "utf8"));
3973
+ writeFileSync(clientPath, bundled, "utf8");
3974
+ if (
3975
+ /^import\s+.*["']@githolon\/client/m.test(bundled) ||
3976
+ /export\s*\{[^}]*nomosInternalOpenGeneratedApplication/s.test(bundled)
3977
+ ) {
3978
+ fail("generated TypeScript application did not seal its private runtime binding");
3979
+ }
2784
3980
  } catch (e) {
2785
- fail(`generated TypeScript client is not syntactically valid — this is a Nomos codegen bug: ${(e as Error).message}`);
3981
+ fail(`generated TypeScript application could not be sealed — this is a Nomos codegen bug: ${(e as Error).message}`);
3982
+ } finally {
3983
+ rmSync(temporaryClientPath, { force: true });
3984
+ rmSync(temporaryRuntimeTypesPath, { force: true });
3985
+ rmSync(temporaryDeclarationPath, { force: true });
3986
+ rmSync(path.join(outDir, `${cfg.name}.client.ts`), { force: true });
2786
3987
  }
2787
- writeFileSync(clientPath, clientSource, "utf8");
2788
3988
 
2789
3989
  // ── 6. the typed Dart frontend (opt-in; the Flutter sibling of step 5, emitted
2790
3990
  // after the package hash like the TS client) ──
@@ -2830,7 +4030,10 @@ async function main(): Promise<void> {
2830
4030
  // COMPILE ATOMICITY (co2): the Dart emit is transactional (beginAtomicGenDir) — a mid-emit fail()/throw
2831
4031
  // restores the prior good packages instead of leaving them wiped/partial. Finalized at process exit.
2832
4032
  if (dartPackages !== undefined) {
2833
- const r = emitDartPackages(presentationDomainModules, dartOut, domainHash, packageInterface, dartPackages, dartNames, nomosCompat, cfg.name);
4033
+ const r = emitDartPackages(
4034
+ presentationDomainModules, dartOut, domainHash, packageInterface, dartPackages,
4035
+ dartNames, nomosCompat, cfg.name, deployPath,
4036
+ );
2834
4037
  dartFiles = r.written;
2835
4038
  dartCursedHints = r.cursed;
2836
4039
  } else {
@@ -2889,8 +4092,9 @@ async function main(): Promise<void> {
2889
4092
  if (selfDir.includes(`${path.sep}node_modules${path.sep}`)) return undefined;
2890
4093
  const client = path.resolve(selfDir, "..", "dart");
2891
4094
  const types = path.resolve(selfDir, "..", "dart_types");
4095
+ const flutter = path.resolve(selfDir, "..", "..", "nomos_flutter");
2892
4096
  if (existsSync(path.join(client, "pubspec.yaml")) && existsSync(path.join(types, "pubspec.yaml"))) {
2893
- return { client, types };
4097
+ return { client, types, ...(existsSync(path.join(flutter, "pubspec.yaml")) ? { flutter } : {}) };
2894
4098
  }
2895
4099
  return undefined;
2896
4100
  })();
@@ -2903,6 +4107,7 @@ async function main(): Promise<void> {
2903
4107
  const lines = ["dependency_overrides:"];
2904
4108
  const usesClient = /(^|\n)\s+nomos_client\s*:/.test(spec);
2905
4109
  const usesTypes = /(^|\n)\s+nomos_types\s*:/.test(spec);
4110
+ const usesFlutter = /(^|\n)\s+nomos_flutter\s*:/.test(spec);
2906
4111
  if (usesClient) lines.push(` nomos_client:`, ` path: ${localDart.client}`);
2907
4112
  // A generated CLIENT or TEST package may only mention nomos_client directly, but that local
2908
4113
  // package depends on the simultaneously-developed nomos_types. Pub overrides do not propagate
@@ -2911,6 +4116,11 @@ async function main(): Promise<void> {
2911
4116
  // analyzer ever sees the code — exactly the package-skew ceremony this local dogfood gate exists
2912
4117
  // to absorb for tenant developers.
2913
4118
  if (usesTypes || usesClient) lines.push(` nomos_types:`, ` path: ${localDart.types}`);
4119
+ // The generated client application imports nomos_flutter, and a TEST package reaches that
4120
+ // application transitively through its generated client sibling. Overrides do not propagate,
4121
+ // so every local nomos_client solve root must also seat the matching local Flutter host even
4122
+ // when its own pubspec does not name nomos_flutter directly.
4123
+ if ((usesFlutter || usesClient) && localDart.flutter) lines.push(` nomos_flutter:`, ` path: ${localDart.flutter}`);
2914
4124
  if (lines.length > 1) writeFileSync(path.join(pkg, "pubspec_overrides.yaml"), lines.join("\n") + "\n", "utf8");
2915
4125
  }
2916
4126
  }
@@ -2962,7 +4172,12 @@ async function main(): Promise<void> {
2962
4172
  console.log(` domainHash ${domainHash}`);
2963
4173
  if (layeredRootPath !== undefined) {
2964
4174
  console.log(
2965
- ` layered ${rel(layeredRootPath)} (${layeredFileCount} module layer(s) in build/layers/ — flatten-verified byte-identical to the canonical IR)`,
4175
+ ` layered ${rel(layeredRootPath)} (${layeredFileCount} module layer(s) in build/layers/ — Nomos flatten resolves the canonical law)`,
4176
+ );
4177
+ console.log(
4178
+ usdcatFlattenVerified
4179
+ ? ` usdcat ✓ stock OpenUSD --flatten resolves the same canonical law`
4180
+ : ` usdcat ⚠ unavailable — set NOMOS_REQUIRE_USDCAT=1 in release CI to make this proof mandatory`,
2966
4181
  );
2967
4182
  }
2968
4183
  {
@@ -2974,9 +4189,6 @@ async function main(): Promise<void> {
2974
4189
  // from a plan's deterministic secondary `ensure(...)` — the id-mint-gate DX made visible (the author
2975
4190
  // wrote `ensure(B, …)`; the compiler declared `Ensure → B` so the deterministic id admits).
2976
4191
  for (const m of presentationDomainModules) {
2977
- for (const ty of m.autoDeclaredEnsures ?? []) {
2978
- console.log(` ensure auto-declared Ensure→${ty} in '${m.domain ?? m.name}' from a plan's secondary ensure(...) (id-mint gate opened)`);
2979
- }
2980
4192
  }
2981
4193
  const presentedIdentityNames = new Set(presentationDomainModules.map((m) => m.domain ?? m.name));
2982
4194
  const presentedHashes = Object.entries(identity.hashes).filter(([domain]) => presentedIdentityNames.has(domain));
@@ -3072,19 +4284,6 @@ async function main(): Promise<void> {
3072
4284
  }
3073
4285
  }
3074
4286
  console.log(` deploy ${rel(deployPath)}`);
3075
- if (compact) {
3076
- // COMPACT PACKAGES ride a base64 blob (nomos:usdJsonB64) — a peer needs the b64 decode arm
3077
- // (kernel ≥ the 2026-07-04 release; every current cloud/browser/node runtime has it). A NATIVE
3078
- // (iOS/macOS AOT) client pins its OWN kernel binary: if that xcframework predates the arm, install
3079
- // fails with `parse embedded USDA IR: domain package missing USDA field nomos:usdJsonHex`. Flag it
3080
- // so the tenant isn't surprised (the requested "flag compact when targeting native").
3081
- console.log(``);
3082
- console.log(` ⚠ compact package (base64 blob): requires a kernel with the b64 decode arm (≥ 2026-07-04).`);
3083
- console.log(` Current cloud/browser/node runtimes have it. A NATIVE client (nomos_flutter iOS/macOS)`);
3084
- console.log(` pins its OWN kernel — refetch the current NomosKernel.xcframework (rm ios/Frameworks/`);
3085
- console.log(` NomosKernel.xcframework && pod install), or set { compact: false } for a hex package that`);
3086
- console.log(` parses on every kernel (larger deploy body; may need the signed-upload deploy lane).`);
3087
- }
3088
4287
  console.log(``);
3089
4288
  console.log(`your business path:`);
3090
4289
  if (proofPath !== undefined) {