@githolon/dsl 0.98.3 → 0.100.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.
package/dart/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 0.62.0
2
+
3
+ - Generated business clients expose one `birth(payload)` method. The bound session supplies its verified
4
+ principal from the authentication token, and Nomos owns the identity, provenance, persistence, and
5
+ recovery ceremony beneath the business call.
6
+
1
7
  ## 0.61.3
2
8
 
3
9
  - cloudBound seeds the KEYLESS enroll-once delegation (mintKeylessDelegation) so a keyless private home
@@ -829,6 +829,9 @@ class NomosBridge {
829
829
  // caches the per-workspace [NomosSession] handle so `bridge.session(ws)` is stable + cheap.
830
830
  final Set<String> _sessionNames = {};
831
831
  final Map<String, NomosSession> _sessionViews = {};
832
+ // The gate-bound identity retained from each session's auth token. Generated clients consume this so
833
+ // application code never has to thread an actor into ordinary business methods.
834
+ final Map<String, String> _sessionPrincipals = {};
832
835
  // [acquireSession]'s per-workspace single-flight: workspace → the in-flight open every concurrent
833
836
  // acquirer joins (removed when it completes; a live session then hits the fast path instead).
834
837
  final Map<String, Future<NomosSession>> _acquiring = {};
@@ -1079,6 +1082,21 @@ class NomosBridge {
1079
1082
  .map((e) => (e as Map).cast<String, dynamic>())
1080
1083
  .toList();
1081
1084
 
1085
+ String? _principalFromToken(String? token) {
1086
+ if (token == null || token.isEmpty) return null;
1087
+ try {
1088
+ final parts = token.split('.');
1089
+ if (parts.length < 2) return null;
1090
+ final payload = jsonDecode(utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))));
1091
+ final sub = payload is Map ? payload['sub'] : null;
1092
+ if (sub is! String || sub.trim().isEmpty) return null;
1093
+ final value = sub.trim();
1094
+ return value.contains(':') ? value : 'user:$value';
1095
+ } catch (_) {
1096
+ return null;
1097
+ }
1098
+ }
1099
+
1082
1100
  // ── the runtime-aware surface (the boundary the generated Dart client wraps) ──
1083
1101
 
1084
1102
  /// THE PRIMARY MULTI-WORKSPACE ENTRY. Return a [NomosSession] handle pinned to [workspace] — the object a
@@ -1103,6 +1121,16 @@ class NomosBridge {
1103
1121
  List<String> get connectedSessions =>
1104
1122
  List.unmodifiable(_eventPlane._sessionNames);
1105
1123
 
1124
+ /// The identity already proved by this bound session. Null on an unauthenticated/local-only session, or
1125
+ /// on a multi-session plane whose identity would be ambiguous. Generated business methods use this by
1126
+ /// default; an explicit client actor remains an advanced override.
1127
+ String? get principal {
1128
+ final plane = _eventPlane;
1129
+ final scoped = _sessionName;
1130
+ if (scoped != null) return plane._sessionPrincipals[scoped];
1131
+ return plane._sessionPrincipals.length == 1 ? plane._sessionPrincipals.values.first : null;
1132
+ }
1133
+
1106
1134
  /// ENUMERATE THE LOCAL PLANE — every workspace on this device + its lifecycle STATE and sync posture:
1107
1135
  /// `[{ session, workspace, state, mounted, parked, syncPosture }]`. The explicit **`state`** (co2 DX ask)
1108
1136
  /// is the field to branch on — it removes the "listed but `session(ws)` fails on first op" footgun:
@@ -1173,6 +1201,8 @@ class NomosBridge {
1173
1201
  if (onCustodyBreak != null) 'onCustodyBreak': onCustodyBreak.wire,
1174
1202
  }));
1175
1203
  _eventPlane._sessionNames.add(ws); // the open succeeded — the session is real now
1204
+ final boundPrincipal = _principalFromToken(authToken);
1205
+ if (boundPrincipal != null) _eventPlane._sessionPrincipals[ws] = boundPrincipal;
1176
1206
  _eventPlane._lastRecovery = (r['recovery'] as Map?)?.cast<String, Object?>();
1177
1207
  }
1178
1208
 
@@ -1428,6 +1458,12 @@ class NomosBridge {
1428
1458
  .map((m) => (m['workspace'] ?? '').toString())
1429
1459
  .where((s) => s.isNotEmpty),
1430
1460
  ]..removeWhere((s) => s.isEmpty);
1461
+ final birthPrincipal = actor ?? principal;
1462
+ if (birthPrincipal != null && birthPrincipal.isNotEmpty) {
1463
+ for (final born in bornNames) {
1464
+ _eventPlane._sessionPrincipals[born] = birthPrincipal;
1465
+ }
1466
+ }
1431
1467
  // The offer ran on this workspace (NomosRef) — stamp every minted ref with it so it is a complete locator.
1432
1468
  final ws = (r['workspace'] ?? '').toString();
1433
1469
  return NomosOfferOutcome(
package/dart/pubspec.yaml CHANGED
@@ -11,7 +11,7 @@ description: >-
11
11
  Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
12
12
  code-generated per-domain aggregate + directive-payload types. (De)serializes
13
13
  the EXACT serde JSON the Rust kernel + projection produce/consume.
14
- version: 0.61.3
14
+ version: 0.62.0
15
15
 
16
16
  environment:
17
17
  sdk: ^3.5.0
@@ -452,6 +452,21 @@ void main() {
452
452
  expect((req['args'] as Map)['allowUnborn'], isTrue);
453
453
  });
454
454
 
455
+ test('a session retains the authenticated token subject as its default principal', () async {
456
+ await bridge.ready;
457
+ final payload = base64Url
458
+ .encode(utf8.encode(jsonEncode({'sub': 'ada'})))
459
+ .replaceAll('=', '');
460
+ await bridge.connect(
461
+ cloud: 'https://c',
462
+ workspace: 'authenticated',
463
+ authToken: 'header.$payload.signature',
464
+ );
465
+
466
+ expect(bridge.session('authenticated').principal, 'user:ada');
467
+ expect(bridge.principal, 'user:ada');
468
+ });
469
+
455
470
  test(
456
471
  'connect / query / queryById / count / sync / lawStatus all wrap the runner',
457
472
  () async {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.98.3",
3
+ "version": "0.100.0",
4
4
  "type": "module",
5
5
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
package/src/births.ts CHANGED
@@ -3,6 +3,11 @@
3
3
  // wasm32-wasip1 artifact {kernel · projection · embedded
4
4
  // QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
5
5
 
6
+ import type { z } from "zod";
7
+ import type { AggregateHandle } from "./aggregate.js";
8
+ import type { AggregateRef } from "./authoring.js";
9
+ import type { Field } from "./fields.js";
10
+
6
11
  /**
7
12
  * THE BIRTH EFFECT — birth is an EXPLICIT, DECLARED kernel primitive (G3, the unix model).
8
13
  *
@@ -51,32 +56,71 @@ export function __drainBirths(): BirthRecipe[] {
51
56
  * chains carry no key, so they replay byte-green and the kernel does nothing). A plan may birth at most one
52
57
  * child per intent (one offer, one defined consequence — the unix model).
53
58
  *
54
- * TO BIRTH A LEAF CHILD RUNNING ITS OWN LAW (a home, an estate child) — copy `framework/home.ts`'s
55
- * `birthHome`. The recipe is just the two-layer law install + your own seed step; there is NO helper to
56
- * fabricate it and you do NOT need a new primitive (recursion is the law — a home births children exactly
57
- * as root births platforms). `buildGenesisChain` is the ONE builder for the PLATFORM shape (it adds the
58
- * `seedGenesis → grantAdmin → birthCloud` tail); a leaf just omits that tail:
59
+ * This is framework implementation plumbing. Application packages declare
60
+ * `birth(Aggregate, spec)` and never construct a recipe.
61
+ */
62
+ /**
63
+ * A business thing that is important enough to have its own durable, sovereign life.
59
64
  *
60
- * ```ts
61
- * export const birthEstateChild = directive("birthEstateChild")
62
- * .creates(EstateChild)
63
- * .payload(z.object({ child: z.string(), owner: z.string(),
64
- * frameworkHash: z.string(), lawHash: z.string(), bornAt: z.string() }))
65
- * .plan((p) => {
66
- * const actor = "user:" + p.owner;
67
- * birth({ workspace: p.child, genesisChain: [
68
- * { domain: "bootstrap", directiveId: "installDomain", actor, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
69
- * { domain: "nomos", directiveId: "installDomain", actor, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
70
- * { domain: "estate", directiveId: "seedEstateChild", actor, payload: { owner: p.owner, bornAt: p.bornAt } },
71
- * ] });
72
- * return [];
73
- * })
74
- * .births() // the kernel's run_birth_effect folds the recipe through the CHILD's own gate
75
- * .requires("creator"); // grant the parent's owner #creator (self-bound) — the birth authority
76
- * ```
65
+ * This is the ONLY tenant-facing birth declaration. The composer lowers it to the framework's
66
+ * recipe/directive machinery; application code gets one generated `birth(payload)` method. Security,
67
+ * law selection, lineage, storage and recovery are deliberately absent from this shape.
77
68
  */
78
- export function birth(recipe: BirthRecipe): void {
79
- footprint.push(recipe);
69
+ export interface BusinessBirthDecl<
70
+ P extends Record<string, unknown> = Record<string, unknown>,
71
+ F extends Record<string, Field> = Record<string, Field>,
72
+ > {
73
+ readonly __isBusinessBirth: true;
74
+ readonly aggregate: AggregateHandle<string, F>;
75
+ readonly payloadSchema: z.ZodObject<z.ZodRawShape>;
76
+ /** A stable business key the caller already knows (an estate reference, account number, etc.). */
77
+ readonly key: (payload: P) => string;
78
+ /** Author the born thing's initial business facts. Nomos supplies the durable identity and lifecycle. */
79
+ readonly plan: (root: AggregateRef<F>, payload: P) => unknown;
80
+ /** Optional business noun used only to make the durable address readable. Defaults to the aggregate id. */
81
+ readonly namespace?: string;
82
+ }
83
+
84
+ export interface BusinessBirthSpec<
85
+ P extends Record<string, unknown>,
86
+ F extends Record<string, Field>,
87
+ > {
88
+ readonly payload: z.ZodType<P> & z.ZodObject<z.ZodRawShape>;
89
+ readonly key: (payload: P) => string;
90
+ readonly plan: (root: AggregateRef<F>, payload: P) => unknown;
91
+ readonly namespace?: string;
92
+ }
93
+
94
+ /** @internal Record an already-lowered child recipe during a framework directive plan. Application law
95
+ * declares `birth(Aggregate, spec)` and leaves the recipe, identity, and recovery machinery to Nomos. */
96
+ export function birth(recipe: BirthRecipe): void;
97
+ /** Declare that one business aggregate is given its own durable life. */
98
+ export function birth<P extends Record<string, unknown>, F extends Record<string, Field>>(
99
+ aggregate: AggregateHandle<string, F>,
100
+ spec: BusinessBirthSpec<P, F>,
101
+ ): BusinessBirthDecl<P, F>;
102
+ export function birth<P extends Record<string, unknown>, F extends Record<string, Field>>(
103
+ recipeOrAggregate: BirthRecipe | AggregateHandle<string, F>,
104
+ spec?: BusinessBirthSpec<P, F>,
105
+ ): void | BusinessBirthDecl<P, F> {
106
+ if (spec !== undefined) {
107
+ const aggregate = recipeOrAggregate as AggregateHandle<string, F>;
108
+ if (!aggregate || aggregate.__isAggregateHandle !== true) {
109
+ throw new Error("birth(Aggregate, spec): the first argument must be an aggregate handle");
110
+ }
111
+ if (!spec.payload || typeof (spec.payload as { safeParse?: unknown }).safeParse !== "function") {
112
+ throw new Error("birth(Aggregate, spec): spec.payload must be a z.object(...) schema");
113
+ }
114
+ return {
115
+ __isBusinessBirth: true,
116
+ aggregate,
117
+ payloadSchema: spec.payload,
118
+ key: spec.key,
119
+ plan: spec.plan,
120
+ ...(spec.namespace !== undefined ? { namespace: spec.namespace } : {}),
121
+ };
122
+ }
123
+ footprint.push(recipeOrAggregate as BirthRecipe);
80
124
  }
81
125
 
82
126
  // `nomos.sha256` — the trustless pure hash bound in the sealed engine (like `nomos.mint`), used to derive a
@@ -118,37 +162,20 @@ interface MintedRef {
118
162
  }
119
163
 
120
164
  /**
121
- * `birth.keyed(...)` — THE way to birth a child workspace. A workspace exists to be USED, and using it means
122
- * FINDING IT AGAIN, so its name is ALWAYS derived from a `key` the caller owns (a business id, an owner uid)
123
- * via {@link workspaceNameForKey}. The client re-derives the SAME name with `deriveWorkspaceName(namespace,
124
- * key)` — so the born workspace is addressable from the key alone, with zero persistence. Same key ⇒ same
125
- * workspace (re-birth is idempotent). This is the only birth verb you need.
165
+ * @deprecated Tenant law should declare `birth(Aggregate, spec)`. This framework helper remains for the
166
+ * built-in framework laws that implement that declaration.
126
167
  *
127
- * ```ts
128
- * export const birthEstate = directive("birthEstate")
129
- * .creates(EstateBirth)
130
- * .payload(z.object({ estateId: z.string(), owner: z.string(), frameworkHash: z.string(), lawHash: z.string(), bornAt: z.string() }))
131
- * .plan((p) => {
132
- * birth.keyed({ key: p.estateId, namespace: "estate", genesisChain: (ws) => [
133
- * { domain: "bootstrap", directiveId: "installDomain", actor: p.owner, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
134
- * { domain: "nomos", directiveId: "installDomain", actor: p.owner, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
135
- * { domain: "estate", directiveId: "seedEstate", actor: p.owner, payload: { estateId: p.estateId, workspaceName: ws, bornAt: p.bornAt } },
136
- * ] });
137
- * return [];
138
- * })
139
- * .births().requires("estateCreator");
140
- * // app (Dart / TS): final ws = client.deriveWorkspaceName('estate', estateId); // open it, any time, any device
141
- * ```
168
+ * Internal compatibility helper: derives a child address from a stable key. Application packages use
169
+ * `birth(Aggregate, spec)`; generated clients expose only `birth(businessFacts)`.
142
170
  */
143
171
  export const birthKeyed = ({
144
172
  key,
145
173
  namespace,
146
174
  genesisChain,
147
175
  }: {
148
- /** The id you already own that this workspace is FOR (an estateId, an owner uid). The client addresses the
149
- * born workspace by re-deriving its name from this same key — so pick a key the client will have in hand. */
176
+ /** A stable id from which the framework can re-derive the durable address. */
150
177
  key: string;
151
- /** The workspace-name namespace (e.g. `"estate"` → `estate-<hash>`). Groups a domain's children. */
178
+ /** Internal address namespace. */
152
179
  namespace: string;
153
180
  genesisChain: GenesisChainOrBuilder;
154
181
  }): string => {
@@ -177,6 +204,8 @@ export const birthInstance = ({
177
204
  };
178
205
 
179
206
  /**
207
+ * @deprecated Tenant law should declare `birth(Aggregate, spec)`. This is framework compatibility sugar.
208
+ *
180
209
  * `birth.singleton(...)` — a keyed birth whose key is a NATURAL single key (one home per user: key = owner).
181
210
  * Sugar over {@link birthKeyed}: `birth.singleton({ keyedBy: owner, namespace: "home" })`. Same addressing
182
211
  * story — the client re-derives with `deriveWorkspaceName(namespace, keyedBy)`.
@@ -200,7 +229,9 @@ export const birthSingleton = ({
200
229
  // `birth.keyed(...)` — with full types AND the runtime assignment in one place.
201
230
  // eslint-disable-next-line @typescript-eslint/no-namespace
202
231
  export namespace birth {
232
+ /** @deprecated Tenant law should use `birth(Aggregate, spec)`. Framework compatibility only. */
203
233
  export const keyed = birthKeyed;
234
+ /** @deprecated Tenant law should use `birth(Aggregate, spec)`. Framework compatibility only. */
204
235
  export const singleton = birthSingleton;
205
236
  /** @deprecated Use `birth.keyed` — see {@link birthInstance}. */
206
237
  export const instance = birthInstance;
@@ -80,6 +80,7 @@ import { parentAttestedBirthMarker } from "./births.js";
80
80
  import { discoverUnsanctionedEnsures, sanctioningEnsureDirective } from "./secondary_ensure.js";
81
81
  import { assertIdOfLint } from "./idof_lint.js";
82
82
  import { assertSurfacedResolvers } from "./surfaced_lint.js";
83
+ import { expandBusinessBirthExports } from "./business_birth.js";
83
84
 
84
85
  // ─────────────────────────────────────────────────────────────────────────────────
85
86
  // composeDomainModule — the named-keys `module(...)`
@@ -173,6 +174,7 @@ export interface DomainModuleSpec {
173
174
  export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
174
175
  let merged: Mod = {};
175
176
  for (const m of spec.modules) merged = { ...merged, ...m };
177
+ merged = expandBusinessBirthExports(merged, spec.domain ?? spec.name);
176
178
  // `capability()` bundles expand into discoverable entries (the package plane's
177
179
  // merge point) — the quartet validation below then passes without re-exports.
178
180
  merged = expandCapabilityExports(merged);
@@ -0,0 +1,274 @@
1
+ // NOMOS — one business birth at the tenant surface; compute-science mechanics lower here.
2
+
3
+ import { z } from "zod";
4
+ import { ensure } from "./authoring.js";
5
+ import { directive, type Directive } from "./directive.js";
6
+ import {
7
+ birth,
8
+ requireParentAttestedBirth,
9
+ workspaceNameForKey,
10
+ type BusinessBirthDecl,
11
+ type ParentAttestedBirthMarker,
12
+ } from "./births.js";
13
+ import { readFrameworkDeclared } from "./read.js";
14
+ import { AuthProvider, authProvider, setAuthProvider } from "./framework/auth_provider_law.js";
15
+ import {
16
+ BirthGrant,
17
+ BirthPolicy,
18
+ NomosBirthRecord,
19
+ birthGrantByPrincipal,
20
+ birthPolicy,
21
+ birthRecordsByPrincipal,
22
+ } from "./birth_meter.js";
23
+ import { nomosBirthRecordEnsure } from "./birth_meter_ensure.js";
24
+ import { GENESIS_WARRANT_ID, GenesisWarrant, seedGenesis } from "./framework/genesis.js";
25
+ import {
26
+ BIRTH_ATTESTATION_ID,
27
+ BirthAttestation,
28
+ birthAttestation,
29
+ seedBirthAttestation,
30
+ } from "./framework/home.js";
31
+ import { RelationTuple, relationTuplesByObject } from "./framework/relations.js";
32
+ import {
33
+ SignerDelegation,
34
+ enrollDelegation,
35
+ enrollFirstDevice,
36
+ enrollSigner,
37
+ identityRelations,
38
+ signerDelegationByUser,
39
+ } from "./framework/signing_identity.js";
40
+
41
+ /** Private recipe selectors. The kernel resolves these against the parent's current installed law. */
42
+ export const CURRENT_LAW_PREFIX = "nomos:current-law:";
43
+
44
+ export interface BusinessBirthMetadata {
45
+ readonly aggregateId: string;
46
+ readonly publicPayloadSchema: z.ZodObject<z.ZodRawShape>;
47
+ readonly rootId: string;
48
+ readonly namespace: string;
49
+ }
50
+
51
+ export interface BusinessBirthDirective extends Directive<Record<string, unknown>> {
52
+ readonly businessBirth: BusinessBirthMetadata;
53
+ }
54
+
55
+ type BusinessBirthInternalDirective<P = Record<string, unknown>> = Directive<P> & {
56
+ readonly businessBirthInternal: true;
57
+ };
58
+
59
+ function internalDirective<P>(d: Directive<P>): BusinessBirthInternalDirective<P> {
60
+ return { ...d, businessBirthInternal: true } as BusinessBirthInternalDirective<P>;
61
+ }
62
+
63
+ /** Presentation-only marker: the declaration remains signed law, but tenant codegen omits it. */
64
+ function internalSurface<T extends object>(value: T): T & { readonly businessBirthInternal: true } {
65
+ return { ...value, businessBirthInternal: true };
66
+ }
67
+
68
+ const INTERNAL_FIELDS = ["principal", "bornAt", "ownerKeyHash", "birthCert"] as const;
69
+ const sha256Hex = z.string().regex(/^[0-9a-f]{64}$/);
70
+
71
+ /**
72
+ * Every business-born workspace verifies its own travelling birth certificate against its warrant owner.
73
+ * The locators are framework state, not fields the tenant must add to their business aggregate.
74
+ */
75
+ const businessParentAttestedBirth: ParentAttestedBirthMarker = {
76
+ ...requireParentAttestedBirth,
77
+ ownerObject: GENESIS_WARRANT_ID,
78
+ ownerField: "principal",
79
+ certObject: BIRTH_ATTESTATION_ID,
80
+ certField: "certChain",
81
+ };
82
+
83
+ function businessBirthsOf(mod: Record<string, unknown>): BusinessBirthDecl[] {
84
+ return Object.values(mod).filter(
85
+ (v): v is BusinessBirthDecl =>
86
+ !!v && typeof v === "object" && (v as { __isBusinessBirth?: unknown }).__isBusinessBirth === true,
87
+ );
88
+ }
89
+
90
+ function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unknown> {
91
+ const fields = (decl.payloadSchema as z.ZodObject<z.ZodRawShape>).shape;
92
+ for (const field of INTERNAL_FIELDS) {
93
+ if (Object.prototype.hasOwnProperty.call(fields, field)) {
94
+ throw new Error(
95
+ `birth(${decl.aggregate.id}): business payload field '${field}' is reserved by Nomos; ` +
96
+ "rename the business field (identity, provenance and lifecycle stay framework-owned)",
97
+ );
98
+ }
99
+ }
100
+
101
+ const stem = decl.aggregate.id.replace(/[^A-Za-z0-9]/g, "");
102
+ const namespace = (decl.namespace ?? decl.aggregate.id).replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
103
+ const rootId = `${namespace}:self`;
104
+ const seedId = `nomosSeed${stem}`;
105
+ const birthId = `nomosBirth${stem}`;
106
+ const publicSchema = decl.payloadSchema as z.ZodObject<z.ZodRawShape>;
107
+ const internalSchema = publicSchema.extend({
108
+ principal: z.string().min(1),
109
+ bornAt: z.string().min(1),
110
+ ownerKeyHash: sha256Hex.optional(),
111
+ birthCert: z.string().optional(),
112
+ });
113
+
114
+ const seedBase = directive(seedId)
115
+ .ensures(decl.aggregate)
116
+ .payload(internalSchema)
117
+ .plan((p) => {
118
+ decl.plan(ensure(decl.aggregate, rootId), p);
119
+ return [];
120
+ });
121
+ const seed = {
122
+ ...seedBase,
123
+ businessBirthInternal: true,
124
+ } as BusinessBirthInternalDirective;
125
+
126
+ const birthBase = directive(birthId)
127
+ .creates(decl.aggregate)
128
+ .payload(internalSchema)
129
+ .plan((p) => {
130
+ const founder = p.principal as string;
131
+ const child = workspaceNameForKey(namespace, String(decl.key(p)));
132
+ const chain: Array<Record<string, unknown>> = [
133
+ {
134
+ domain: "bootstrap",
135
+ directiveId: "installDomain",
136
+ actor: founder,
137
+ payload: { domainHash: `${CURRENT_LAW_PREFIX}nomos` },
138
+ domainHash: `${CURRENT_LAW_PREFIX}nomos`,
139
+ domainPackageB64: "",
140
+ },
141
+ {
142
+ domain: "nomos",
143
+ directiveId: "installDomain",
144
+ actor: founder,
145
+ payload: { domainHash: `${CURRENT_LAW_PREFIX}${domain}` },
146
+ domainHash: `${CURRENT_LAW_PREFIX}${domain}`,
147
+ domainPackageB64: "",
148
+ },
149
+ {
150
+ domain,
151
+ directiveId: seedId,
152
+ actor: founder,
153
+ payload: p,
154
+ },
155
+ ];
156
+
157
+ const apRows = readFrameworkDeclared<Array<Record<string, unknown>>>("authProvider", { scope: "platform" });
158
+ const apRaw = Array.isArray(apRows) && apRows.length > 0 ? apRows[0] : undefined;
159
+ const ap = (apRaw && typeof apRaw === "object" && typeof (apRaw as { data?: unknown }).data === "object"
160
+ ? (apRaw as { data: Record<string, unknown> }).data
161
+ : apRaw) as Record<string, unknown> | undefined;
162
+ const apStr = (k: string): string | undefined => {
163
+ const v = ap?.[k];
164
+ return typeof v === "string" && v.length > 0 ? v : undefined;
165
+ };
166
+ let verified = false;
167
+ try { verified = typeof p.birthCert === "string" && JSON.parse(p.birthCert).verified === true; } catch {}
168
+ if (typeof p.ownerKeyHash === "string") {
169
+ chain.push({
170
+ domain,
171
+ directiveId: "enrollSigner",
172
+ actor: founder,
173
+ payload: {
174
+ keyHash: p.ownerKeyHash,
175
+ grantedBy: founder,
176
+ grantedAt: 1,
177
+ ...(verified && apStr("issuer") ? { verifiedVia: apStr("issuer") } : {}),
178
+ },
179
+ });
180
+ }
181
+ if (apStr("issuer")) {
182
+ chain.push({
183
+ domain,
184
+ directiveId: "setAuthProvider",
185
+ actor: founder,
186
+ payload: {
187
+ issuer: apStr("issuer"),
188
+ jwksUrl: apStr("jwksUrl") ?? apStr("issuer"),
189
+ ...(apStr("keys") ? { keys: apStr("keys") } : {}),
190
+ ...(apStr("audience") ? { audience: apStr("audience") } : {}),
191
+ ...(apStr("principalClaim") ? { principalClaim: apStr("principalClaim") } : {}),
192
+ ...(apStr("rootCert") ? { rootCert: apStr("rootCert") } : {}),
193
+ ...(apStr("deviceBindingClaim") ? { deviceBindingClaim: apStr("deviceBindingClaim") } : {}),
194
+ ...(apStr("requireDeviceBinding") ? { requireDeviceBinding: apStr("requireDeviceBinding") } : {}),
195
+ label: apStr("label") ?? "inherited-from-parent",
196
+ setBy: founder,
197
+ setAt: 1,
198
+ },
199
+ });
200
+ }
201
+ birth({ workspace: child, genesisChain: chain });
202
+ return [];
203
+ })
204
+ .reads(authProvider)
205
+ .births();
206
+
207
+ const metadata: BusinessBirthMetadata = {
208
+ aggregateId: decl.aggregate.id,
209
+ publicPayloadSchema: publicSchema,
210
+ rootId,
211
+ namespace,
212
+ };
213
+ const parent = { ...birthBase, businessBirth: metadata } as BusinessBirthDirective;
214
+ return {
215
+ [`__nomos_${birthId}`]: parent,
216
+ [`__nomos_${seedId}`]: seed,
217
+
218
+ // THE SOVEREIGN-WORKSPACE CORE. These are ordinary law declarations, composed automatically so the
219
+ // tenant's one business declaration is also a complete child law: it can fold the kernel-injected
220
+ // travelling attestation + warrant, seat its founding device, inherit its verifier, and verify itself
221
+ // offline after custody is moved or reopened. The generated tenant client hides every one of them.
222
+ __nomos_GenesisWarrant: internalSurface(GenesisWarrant),
223
+ __nomos_seedGenesis: internalDirective(seedGenesis),
224
+ __nomos_BirthAttestation: internalSurface(BirthAttestation),
225
+ __nomos_seedBirthAttestation: internalDirective(seedBirthAttestation),
226
+ __nomos_birthAttestation: internalSurface(birthAttestation),
227
+ __nomos_RelationTuple: internalSurface(RelationTuple),
228
+ __nomos_relationTuplesByObject: internalSurface(relationTuplesByObject),
229
+ __nomos_identityRelations: identityRelations,
230
+ __nomos_enrollSigner: internalDirective(enrollSigner),
231
+ __nomos_enrollFirstDevice: internalDirective(enrollFirstDevice),
232
+ __nomos_SignerDelegation: internalSurface(SignerDelegation),
233
+ __nomos_enrollDelegation: internalDirective(enrollDelegation),
234
+ __nomos_signerDelegationByUser: internalSurface(signerDelegationByUser),
235
+ __nomos_AuthProvider: internalSurface(AuthProvider),
236
+ __nomos_setAuthProvider: internalDirective(setAuthProvider),
237
+ __nomos_authProvider: internalSurface(authProvider),
238
+ // Exact export keys prevent the generic `.births()` auto-compose seam from inserting a second,
239
+ // unmarked copy. The values are still the canonical framework declarations.
240
+ NomosBirthRecord: internalSurface(NomosBirthRecord),
241
+ BirthPolicy: internalSurface(BirthPolicy),
242
+ BirthGrant: internalSurface(BirthGrant),
243
+ nomosBirthRecordEnsure: internalDirective(nomosBirthRecordEnsure),
244
+ __nomos_birthRecordsByPrincipal: internalSurface(birthRecordsByPrincipal),
245
+ __nomos_birthPolicy: internalSurface(birthPolicy),
246
+ __nomos_birthGrantByPrincipal: internalSurface(birthGrantByPrincipal),
247
+ __nomos_parentAttestedBirth: businessParentAttestedBirth,
248
+ };
249
+ }
250
+
251
+ /** Lower the one tenant-facing birth declaration into ordinary, gate-visible framework directives. */
252
+ export function expandBusinessBirthExports<M extends Record<string, unknown>>(
253
+ mod: M,
254
+ domain: string,
255
+ ): Record<string, unknown> {
256
+ const declarations = businessBirthsOf(mod);
257
+ if (declarations.length === 0) return mod;
258
+ if (declarations.length > 1) {
259
+ throw new Error(
260
+ `domain '${domain}' declares ${declarations.length} business births; one generated birth(payload) cannot be ` +
261
+ "ambiguous — model one born aggregate root per domain",
262
+ );
263
+ }
264
+ const lowered = lowerOne(declarations[0]!, domain);
265
+ return { ...mod, ...lowered };
266
+ }
267
+
268
+ export function isBusinessBirthDirective(value: unknown): value is BusinessBirthDirective {
269
+ return !!value && typeof value === "object" && !!(value as { businessBirth?: unknown }).businessBirth;
270
+ }
271
+
272
+ export function isBusinessBirthInternalDirective(value: unknown): boolean {
273
+ return !!value && typeof value === "object" && (value as { businessBirthInternal?: unknown }).businessBirthInternal === true;
274
+ }
@@ -4243,8 +4243,36 @@ export interface GenerateDartDomainOptions {
4243
4243
 
4244
4244
  /** Generate the full Dart source for one domain module. */
4245
4245
  export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOptions = {}): string {
4246
+ const publicAggregates = mod.aggregates.filter(
4247
+ (a) => (a as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
4248
+ );
4249
+ const publicAggregateIds = new Set(publicAggregates.map((a) => a.id));
4250
+ mod = {
4251
+ ...mod,
4252
+ aggregates: publicAggregates,
4253
+ directives: mod.directives.filter(
4254
+ (d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
4255
+ ),
4256
+ ...(mod.queries === undefined
4257
+ ? {}
4258
+ : { queries: mod.queries.filter((q) => (q as { businessBirthInternal?: unknown }).businessBirthInternal !== true) }),
4259
+ ...(mod.readModelAggregates === undefined
4260
+ ? {}
4261
+ : { readModelAggregates: mod.readModelAggregates.filter((id) => publicAggregateIds.has(id)) }),
4262
+ };
4246
4263
  registerDartNames(mod.aggregates); // `.dartName` overrides for this domain's Dart naming derivations.
4247
4264
  const domainKey = mod.domain ?? mod.name;
4265
+ // ONE BUSINESS BIRTH: framework-only genesis/control directives remain law, but are not tenant DTOs.
4266
+ // The one public parent directive is rendered against the business payload schema (identity/provenance
4267
+ // fields are injected by the runtime). This is frontend codegen only; the composed manifest is untouched.
4268
+ const codegenDirectives = mod.directives
4269
+ .filter((d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true)
4270
+ .map((d) => {
4271
+ const businessBirth = (d as { businessBirth?: { publicPayloadSchema?: z.ZodTypeAny } }).businessBirth;
4272
+ return businessBirth?.publicPayloadSchema === undefined
4273
+ ? d
4274
+ : { ...d, payloadSchema: businessBirth.publicPayloadSchema } as AnyDirective;
4275
+ });
4248
4276
  const skipAggregateIds = opts.skipAggregateIds ?? new Set<string>();
4249
4277
  const skipDirectiveIds = opts.skipDirectiveIds ?? new Set<string>();
4250
4278
  const skipQueryIds = opts.skipQueryIds ?? new Set<string>();
@@ -4255,11 +4283,11 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
4255
4283
  // their generated class — collected from the branded payload schemas, deduped by name against
4256
4284
  // the aggregate-derived set. Codegen-only: the IR key stays aggregate-derived (era rule).
4257
4285
  const aggVoNames = new Set((mod.valueObjects ?? []).map((d) => d.name));
4258
- const payloadVos = collectPayloadValueObjectDescriptors(mod.directives ?? []).filter((d) => !aggVoNames.has(d.name));
4286
+ const payloadVos = collectPayloadValueObjectDescriptors(codegenDirectives).filter((d) => !aggVoNames.has(d.name));
4259
4287
  // RECURSIVE `z.lazy` payloads → synthesized recursive-class descriptors (full support). They
4260
4288
  // are codegen-only (like payloadVos): the law IR / domainHash never sees them (era rule). The
4261
4289
  // name map routes each recursive payload field to its class in [emitDirective].
4262
- const recursiveTypes = synthesizeRecursivePayloadTypes(mod.directives ?? []);
4290
+ const recursiveTypes = synthesizeRecursivePayloadTypes(codegenDirectives);
4263
4291
  const recursiveDescriptors = [...recursiveTypes.descriptors.values()].filter(
4264
4292
  (d) => !aggVoNames.has(d.name) && !payloadVos.some((p) => p.name === d.name),
4265
4293
  );
@@ -4289,7 +4317,7 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
4289
4317
  if ("fields" in arm) for (const f of Object.values(arm.fields)) walkUnitField(f);
4290
4318
  }
4291
4319
  }
4292
- for (const dir of mod.directives ?? []) {
4320
+ for (const dir of codegenDirectives) {
4293
4321
  const schema = (dir as { payloadSchema?: unknown }).payloadSchema;
4294
4322
  if (schema && typeof schema === "object" && zodTypeName(schema as z.ZodTypeAny) === "ZodObject") {
4295
4323
  try {
@@ -4353,7 +4381,7 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
4353
4381
  f.valueObject === undefined),
4354
4382
  ),
4355
4383
  );
4356
- const localDirectives = mod.directives.filter((d) => !skipDirectiveIds.has(d.id));
4384
+ const localDirectives = codegenDirectives.filter((d) => !skipDirectiveIds.has(d.id));
4357
4385
  const localQueries = (mod.queries ?? []).filter((q) => !skipQueryIds.has(q.id));
4358
4386
  // Enums are collected from the LOCALLY-emitted symbols only — a framework directive/aggregate's
4359
4387
  // enums ride the shared `nomos_framework.dart`, so a domain that skips them emits no dangling enum.
@@ -4367,7 +4395,7 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
4367
4395
  for (const a of mod.aggregates) aggregatesById.set(a.id, a);
4368
4396
  const ownIdFieldsByAggregate = collectOwnIdFieldsByAggregate(
4369
4397
  mod.aggregates,
4370
- mod.directives,
4398
+ codegenDirectives,
4371
4399
  dartTypes,
4372
4400
  );
4373
4401
  const enumBlock = decls.join("\n\n");
@@ -167,6 +167,23 @@ export interface DartBridgeClientOptions {
167
167
 
168
168
  /** Generate the `<name>.client.dart` async bridge client for one domain module. */
169
169
  export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClientOptions): string {
170
+ const publicAggregates = mod.aggregates.filter(
171
+ (a) => (a as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
172
+ );
173
+ const publicAggregateIds = new Set(publicAggregates.map((a) => a.id));
174
+ mod = {
175
+ ...mod,
176
+ aggregates: publicAggregates,
177
+ directives: mod.directives.filter(
178
+ (d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
179
+ ),
180
+ ...(mod.queries === undefined
181
+ ? {}
182
+ : { queries: mod.queries.filter((q) => (q as { businessBirthInternal?: unknown }).businessBirthInternal !== true) }),
183
+ ...(mod.readModelAggregates === undefined
184
+ ? {}
185
+ : { readModelAggregates: mod.readModelAggregates.filter((id) => publicAggregateIds.has(id)) }),
186
+ };
170
187
  registerDartNames(mod.aggregates); // `.dartName` overrides for read-model class names in the client.
171
188
  const domain = mod.domain ?? mod.name;
172
189
  const className = dartClientClassName(domain);
@@ -223,7 +240,9 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
223
240
  // cross-workspace governance offer to a PARENT. It gets a typed `bridge.createWorkspace(...)` wrapper
224
241
  // (below), NOT an `offerOutcome`; excluded here so a frontend dev can never call it as a local write.
225
242
  const allDirs = (mod.directives as AnyDirective[]).filter(
226
- (d) => d && typeof d.id === "string" && typeof (d as { payloadSchema?: unknown }).payloadSchema === "object",
243
+ (d) => d && typeof d.id === "string"
244
+ && typeof (d as { payloadSchema?: unknown }).payloadSchema === "object"
245
+ && (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
227
246
  );
228
247
  const birthDirs = allDirs.filter((d) => (d as { birthsChild?: boolean }).birthsChild === true);
229
248
  const dirs = allDirs.filter((d) => (d as { birthsChild?: boolean }).birthsChild !== true);
@@ -324,6 +343,32 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
324
343
  const payloadClass = `${cap(d.id)}Payload`;
325
344
  const method = safeMethod(d.id);
326
345
  const gate = (d as { requiresRelationGate?: { relation: string } }).requiresRelationGate;
346
+ const businessBirth = (d as unknown as {
347
+ businessBirth?: { aggregateId: string; rootId: string };
348
+ }).businessBirth;
349
+ if (businessBirth !== undefined) {
350
+ methods.push(
351
+ ` /// Give one ${businessBirth.aggregateId} its own durable life. Nomos owns identity, authorship,`,
352
+ ` /// provenance, persistence and recovery; the app supplies only business facts.`,
353
+ ` Future<NomosRef> birth(${payloadClass} payload) async {`,
354
+ ` final principal = actor ?? bridge.principal;`,
355
+ ` if (principal == null || principal.trim().isEmpty) {`,
356
+ ` throw MissingOfferAuthority(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, requires: 'a signed-in Nomos session');`,
357
+ ` }`,
358
+ ` final data = payload.toPayloadJson();`,
359
+ ` data['principal'] = principal;`,
360
+ ` data['bornAt'] = DateTime.now().toUtc().toIso8601String();`,
361
+ ` final outcome = await _offer(${dartStr(d.id)}, data);`,
362
+ ` if (outcome.born.length != 1) {`,
363
+ ` throw StateError('birth() committed without returning exactly one born workspace');`,
364
+ ` }`,
365
+ ` await bridge.sync();`,
366
+ ` return NomosRef(workspace: outcome.born.single, type: ${dartStr(businessBirth.aggregateId)}, id: ${dartStr(businessBirth.rootId)});`,
367
+ ` }`,
368
+ ``,
369
+ );
370
+ continue;
371
+ }
327
372
  methods.push(
328
373
  ` /// Births a CHILD WORKSPACE named by \`${payloadClass}\` — a \`.births()\` offer on THIS client's`,
329
374
  ` /// session, whose workspace IS the parent (session_bound_clients.md ask 4: no loose parent string).`,
@@ -99,6 +99,18 @@ export function generateOpenApiFragment(
99
99
  module: DomainModule,
100
100
  opts: OpenApiFragmentOptions = {},
101
101
  ): OpenApiDoc {
102
+ module = {
103
+ ...module,
104
+ aggregates: module.aggregates.filter(
105
+ (a) => (a as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
106
+ ),
107
+ directives: module.directives.filter(
108
+ (d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
109
+ ),
110
+ ...(module.queries === undefined
111
+ ? {}
112
+ : { queries: module.queries.filter((q) => (q as { businessBirthInternal?: unknown }).businessBirthInternal !== true) }),
113
+ };
102
114
  const domain = domainKey(module);
103
115
  const paths: Record<string, Record<string, unknown>> = {};
104
116
 
@@ -111,12 +123,22 @@ export function generateOpenApiFragment(
111
123
 
112
124
  // OFFERABLE INTENTS → POST offers. The payload JSON Schema is the intent's own zod, lowered.
113
125
  for (const dir of module.directives as AnyDirective[]) {
126
+ if ((dir as { businessBirthInternal?: unknown }).businessBirthInternal === true) continue;
127
+ const businessBirth = (dir as {
128
+ businessBirth?: { aggregateId: string; publicPayloadSchema: unknown };
129
+ }).businessBirth;
130
+ const publicId = businessBirth === undefined ? dir.id : "birth";
131
+ const publicDir = businessBirth === undefined
132
+ ? dir
133
+ : { ...dir, payloadSchema: businessBirth.publicPayloadSchema } as AnyDirective;
114
134
  const invs = invariantsOf(dir.id);
115
135
  const op: Record<string, unknown> = {
116
- operationId: `${domain}__${dir.id}`,
136
+ operationId: `${domain}__${publicId}`,
117
137
  tags: [domain],
118
- summary: `offer: ${dir.id}`,
119
- description: dir.marker && dir.aggregateId
138
+ summary: businessBirth === undefined ? `offer: ${dir.id}` : `birth ${businessBirth.aggregateId}`,
139
+ description: businessBirth !== undefined
140
+ ? `Give one ${businessBirth.aggregateId} its own durable life.`
141
+ : dir.marker && dir.aggregateId
120
142
  ? `Offer the \`${dir.id}\` intent (${dir.marker} ${dir.aggregateId}). Admitted or refused by the law; the plan is private.`
121
143
  : `Offer the \`${dir.id}\` intent. Admitted or refused by the law.`,
122
144
  "x-nomos": {
@@ -130,7 +152,7 @@ export function generateOpenApiFragment(
130
152
  },
131
153
  requestBody: {
132
154
  required: true,
133
- content: { "application/json": { schema: payloadJsonSchema(dir, module) } },
155
+ content: { "application/json": { schema: payloadJsonSchema(publicDir, module) } },
134
156
  },
135
157
  responses: {
136
158
  "200": { description: "admitted — the sealed intent head" },
@@ -138,7 +160,7 @@ export function generateOpenApiFragment(
138
160
  "422": { description: "the law refused the intent (typed refusal)" },
139
161
  },
140
162
  };
141
- (paths[`/${dir.id}`] ??= {}).post = op;
163
+ (paths[`/${publicId}`] ??= {}).post = op;
142
164
  }
143
165
 
144
166
  // QUERIES → GET reads. Declared queries + the implicit hasMany inverse indexes (the relation IS
@@ -1720,6 +1720,9 @@ interface DiscoveredGenesisStep {
1720
1720
  * synthesized payload (the synthesizer only produces `sample-*` placeholders, which the child install
1721
1721
  * would refuse). */
1722
1722
  export interface ChildBirthLeg {
1723
+ /** Present for the one application-facing `birth(Aggregate, spec)` declaration. Proof renderers use the
1724
+ * business noun and keep the lowered framework ceremony out of normal output. */
1725
+ readonly businessAggregateId?: string;
1723
1726
  /** The domain the `.births()` directive lives in. */
1724
1727
  readonly parentDomain: string;
1725
1728
  /** The `.births()` directive id the parent drives to birth the child. */
@@ -2021,9 +2024,11 @@ export function discoverChildBirthLegOrReason(
2021
2024
  if (frameworkHashField !== undefined) delete birthPayload[frameworkHashField];
2022
2025
  if (lawHashField !== undefined) delete birthPayload[lawHashField];
2023
2026
 
2027
+ const businessAggregateId = (birthDir as unknown as { businessBirth?: { aggregateId?: string } }).businessBirth?.aggregateId;
2024
2028
  const leg: ChildBirthLeg = {
2025
2029
  parentDomain,
2026
2030
  birthDirectiveId: birthDir.id,
2031
+ ...(businessAggregateId !== undefined ? { businessAggregateId } : {}),
2027
2032
  birthPayload,
2028
2033
  birthAutoStamped: synth.autoStamped,
2029
2034
  ...(actorField !== undefined ? { birthActorField: actorField } : {}),
@@ -2346,6 +2351,15 @@ function hasBirthDirective(mod: DomainModule): boolean {
2346
2351
  * when it has a `.creates`; child-birth-provable when it has a `.births()`). NEVER null: every compiled
2347
2352
  * domain is at least `compile-valid` (gaps 1+3 — every domain is proof-addressable). */
2348
2353
  export function legsForDomain(mod: DomainModule, modules: readonly DomainModule[]): ProofLegs {
2354
+ // Business-birth control declarations are proofed as part of the birth's child genesis, not as
2355
+ // tenant-authored standalone/gated actions. Keep the full `modules` below for recipe resolution while
2356
+ // removing the presentation-only internals from leg selection and diagnostics.
2357
+ mod = {
2358
+ ...mod,
2359
+ directives: (mod.directives as Directive<unknown>[]).filter(
2360
+ (d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
2361
+ ),
2362
+ };
2349
2363
  const domain = mod.domain ?? mod.name;
2350
2364
  const allBirths = modules.flatMap((m) => childBirthsForModule(m, modules));
2351
2365
  // The standalone-create leg (when the domain has a non-birth `.creates`). FAIL SOFT: an exotic payload
package/src/codegen_ts.ts CHANGED
@@ -800,6 +800,27 @@ function anyPackedTaxonomy(modules: readonly DomainModule[]): boolean {
800
800
  }
801
801
 
802
802
  export function generateTsClient(modules: readonly DomainModule[], opts: TsClientOptions): string {
803
+ // Business-birth framework declarations remain in the certified module, but are not application
804
+ // vocabulary. Strip only the presentation marker here; manifests, hashes and the engine see the full law.
805
+ modules = modules.map((mod) => {
806
+ const publicAggregates = mod.aggregates.filter(
807
+ (a) => (a as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
808
+ );
809
+ const publicAggregateIds = new Set(publicAggregates.map((a) => a.id));
810
+ return {
811
+ ...mod,
812
+ aggregates: publicAggregates,
813
+ directives: mod.directives.filter(
814
+ (d) => (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
815
+ ),
816
+ ...(mod.queries === undefined
817
+ ? {}
818
+ : { queries: mod.queries.filter((q) => (q as { businessBirthInternal?: unknown }).businessBirthInternal !== true) }),
819
+ ...(mod.readModelAggregates === undefined
820
+ ? {}
821
+ : { readModelAggregates: mod.readModelAggregates.filter((id) => publicAggregateIds.has(id)) }),
822
+ };
823
+ });
803
824
  const hashConst = tsHashConstName(opts.packageName);
804
825
  // THE E2E-SEALING MAP (the sealing client): non-null iff any field is `.encrypted()`. When null,
805
826
  // NOTHING below emits — the generated client is byte-identical to a pre-sealing compile.
@@ -812,7 +833,10 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
812
833
  // A package declaring ≥1 `.births()` directive emits the branded identity wrappers (below) its typed
813
834
  // birth methods take for `actor`/`authorSecret`. Births-free packages emit nothing (byte-identical).
814
835
  const hasBirths = modules.some((m) =>
815
- (m.directives as Directive<unknown>[]).some((d) => (d as { birthsChild?: boolean })?.birthsChild === true),
836
+ (m.directives as Directive<unknown>[]).some((d) =>
837
+ (d as { birthsChild?: boolean })?.birthsChild === true
838
+ && (d as { businessBirth?: unknown }).businessBirth === undefined,
839
+ ),
816
840
  );
817
841
  // SURFACED MERGE CONFLICTS: a package declaring ≥1 `.merge(Surfaced(...))` field emits the
818
842
  // `SurfacedValue<T>` union + guard (below). Surfaced-free packages emit nothing (byte-identical).
@@ -869,6 +893,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
869
893
  ` * aggregate instance: \`{ workspace, type, id }\`, with the stable interop string`,
870
894
  ` * \`nomos://<workspace>/<type>/<id>\`. Carried on every read \`Row.ref\` + write outcome + activity ref. */`,
871
895
  `export interface NomosRef { workspace: string; type: string; id: string }`,
896
+ `/** A newly born business root. The phantom type makes the returned reference aggregate-specific while`,
897
+ ` * its runtime identity stays the one canonical {workspace,type,id} NomosRef. */`,
898
+ `export type NomosBorn<TData> = NomosRef & { readonly __nomosData?: TData };`,
872
899
  `/** The stable \`nomos://<workspace>/<type>/<id>\` string form (each segment URI-encoded — a workspace/`,
873
900
  ` * type/id containing \`/\` or spaces round-trips). Survives URLs, nav params, JSON, logs. */`,
874
901
  `export function nomosRefUri(ref: NomosRef): string {`,
@@ -1025,7 +1052,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1025
1052
  ` /** THE ONE ENCODER offer (the single v2-envelope codec lives in @githolon/client \`encodeIntentOfferBytes\`):`,
1026
1053
  ` * hand the holon a PLANNED directive + the verified \`actor\`; the holon encodes + offers. The generated`,
1027
1054
  ` * client calls THIS, never building offer bytes itself — so there is exactly ONE copy of the wire format. */`,
1028
- ` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string; returnCreated?: boolean; attestations?: readonly { queryId: string; result: unknown; attestation: unknown }[] }): Promise<string | { head: string; created?: { type: string; id: string }[]; workspace?: string }>;`,
1055
+ ` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string; returnCreated?: boolean; attestations?: readonly { queryId: string; result: unknown; attestation: unknown }[] }): Promise<string | { head: string; created?: { type: string; id: string }[]; workspace?: string; born?: unknown; birthOutcomes?: unknown }>;`,
1029
1056
  ` /** Opaque-bytes offer — retained for carriers that already hold an encoded envelope (back-compat). */`,
1030
1057
  ` offer(intentBytes: string | Uint8Array): Promise<string>;`,
1031
1058
  ...(hasBirths
@@ -1038,6 +1065,10 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1038
1065
  : []),
1039
1066
  ` /** The workspace this holon is bound to — the \`workspace\` of every [NomosRef] this client composes. */`,
1040
1067
  ` workspace: string;`,
1068
+ ` /** The identity already proved by this bound session (explicit principal or token subject). */`,
1069
+ ` principal?: string;`,
1070
+ ` /** Persist local custody and reconcile its configured remote. Business birth awaits this when available. */`,
1071
+ ` sync?(): Promise<unknown>;`,
1041
1072
  ...(encPresent
1042
1073
  ? [
1043
1074
  ` /** \`keys\` (E2E sealing): the reader's held scope keys, injected so encrypted fields decrypt;`,
@@ -1808,7 +1839,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1808
1839
  for (const mod of modules) {
1809
1840
  const domain = mod.domain ?? mod.name;
1810
1841
  const dirs = (mod.directives as Directive<unknown>[]).filter(
1811
- (d) => d && typeof d.id === "string" && typeof (d as { payloadSchema?: unknown }).payloadSchema === "object",
1842
+ (d) => d && typeof d.id === "string" && typeof (d as { payloadSchema?: unknown }).payloadSchema === "object"
1843
+ && (d as { businessBirthInternal?: unknown }).businessBirthInternal !== true,
1812
1844
  );
1813
1845
  out.push(`// ── domain "${domain}" ──`, ``);
1814
1846
 
@@ -1849,7 +1881,11 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1849
1881
  if (usedPayloadNames.has(payloadName)) payloadName = `${cap(camel(domain))}${cap(d.id)}Payload`;
1850
1882
  usedPayloadNames.add(payloadName);
1851
1883
  dirPayloadNames.set(d.id, payloadName);
1852
- const schema = (d as unknown as { payloadSchema: z.ZodTypeAny }).payloadSchema;
1884
+ const businessBirth = (d as unknown as {
1885
+ businessBirth?: { aggregateId: string; publicPayloadSchema: z.ZodTypeAny; rootId: string; namespace: string };
1886
+ }).businessBirth;
1887
+ const schema = businessBirth?.publicPayloadSchema
1888
+ ?? (d as unknown as { payloadSchema: z.ZodTypeAny }).payloadSchema;
1853
1889
  const stamps = autoStampFields(schema);
1854
1890
  const mint = mintPlan?.get(d.id);
1855
1891
  const placed = placementDirs.has(d.id) ? ["shard"] : [];
@@ -1897,6 +1933,24 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1897
1933
  `export type ${payloadName} = ${tsTypeOfPayload(schema, stamps, mint ? [mint.field] : [], placed, lazyReg, d.id)};`,
1898
1934
  ``,
1899
1935
  );
1936
+ // ONE BUSINESS BIRTH: the session is the parent; identity, timestamp, child addressing, law selection,
1937
+ // provenance and device seating are internal. The app supplies only the declared business payload and
1938
+ // receives the born root's typed NomosRef.
1939
+ if (businessBirth !== undefined) {
1940
+ methods.push(
1941
+ ` /** Give one ${businessBirth.aggregateId} its own durable life. Nomos owns identity, provenance, persistence and recovery. */`,
1942
+ ` birth: async (payload: ${payloadName}): Promise<NomosBorn<${businessBirth.aggregateId}Data>> => {`,
1943
+ ` const principal = actor ?? holon.principal;`,
1944
+ ` if (!principal || !principal.trim()) throw new Error("birth() needs a signed-in Nomos session");`,
1945
+ ` const result = await holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, { ...payload, principal, bornAt: new Date().toISOString() }, { domainHash, actor: principal, returnCreated: true });`,
1946
+ ` const born = typeof result === "object" && result !== null && Array.isArray(result.born) ? result.born[0] : undefined;`,
1947
+ ` if (typeof born !== "object" || born === null || typeof born.workspace !== "string" || !born.workspace) throw new Error("birth() committed without returning the born workspace");`,
1948
+ ` await holon.sync?.();`,
1949
+ ` return { workspace: born.workspace, type: ${JSON.stringify(businessBirth.aggregateId)}, id: ${JSON.stringify(businessBirth.rootId)} };`,
1950
+ ` },`,
1951
+ );
1952
+ continue;
1953
+ }
1900
1954
  // A `.births()` directive is NOT a same-workspace write — it is a signed CROSS-WORKSPACE governance
1901
1955
  // offer to a PARENT. It gets a typed `holon.createWorkspace(...)` wrapper (declaring domain +
1902
1956
  // directiveId + baked domainHash bound at codegen time), NOT an `offerDirective`, so a frontend dev
@@ -2118,7 +2172,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2118
2172
  ` * judge the real author. Omit it for open (un-gated) domains. */`,
2119
2173
  `export function ${tsClientFactoryName(domain)}(holon: NomosHolon, opts: { domainHash?: string; actor?: string${encPresent ? "; keyStore?: NomosKeyStore" : ""} } = {}) {`,
2120
2174
  ` const domainHash = opts.domainHash ?? ${hashConst};`,
2121
- ` const actor = opts.actor;`,
2175
+ ` const actor = opts.actor ?? holon.principal;`,
2122
2176
  ...(encPresent ? [` const keyStore = opts.keyStore;`] : []),
2123
2177
  ...(opts.compat !== undefined
2124
2178
  ? [
@@ -93,6 +93,7 @@ import {
93
93
  import { synthesizePreconditionInvariants } from "./precondition.js";
94
94
  import { BIRTH_METER_QUERIES, synthesizeBirthQuotaInvariants } from "./birth_meter.js";
95
95
  import { withBirthMeterExports } from "./birth_meter_ensure.js";
96
+ import { expandBusinessBirthExports } from "./business_birth.js";
96
97
  import { expandRoleExports, rolesOf } from "./role.js";
97
98
  import { collectEngineRouting } from "./engine_entry.js";
98
99
  import {
@@ -1051,7 +1052,7 @@ const NOMOS_TYPES_DEP = "^0.6.0";
1051
1052
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1052
1053
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1053
1054
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1054
- const NOMOS_CLIENT_DEP = "^0.61.0";
1055
+ const NOMOS_CLIENT_DEP = "^0.62.0";
1055
1056
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1056
1057
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1057
1058
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -1616,6 +1617,7 @@ async function main(): Promise<void> {
1616
1617
  }
1617
1618
  let merged: Mod = {};
1618
1619
  for (const m of mods) merged = { ...merged, ...m };
1620
+ merged = expandBusinessBirthExports(merged, d.key);
1619
1621
  // `capability()` bundles expand into discoverable entries (the CLI plane's
1620
1622
  // merge point) — queries/sums/sid-minting/manifests all see the pieces.
1621
1623
  merged = expandCapabilityExports(merged);
@@ -2959,19 +2961,16 @@ async function main(): Promise<void> {
2959
2961
  console.log(` parses on every kernel (larger deploy body; may need the signed-upload deploy lane).`);
2960
2962
  }
2961
2963
  console.log(``);
2962
- console.log(`deploy (Nomos Cloud):`);
2963
- console.log(` githolon login --agent && githolon ws create <ws> && githolon deploy <ws> # no secret — deploy is OPEN, kernel-judged at the offer gate`);
2964
- console.log(` # raw lane: curl -X POST -H 'content-type: application/json' -H 'x-nomos-principal: <your-auth-uid>' \\`);
2965
- console.log(` # --data-binary @${rel(deployPath)} https://nomos.captainapp.co.uk/v2/workspaces/<ws>/domains`);
2964
+ console.log(`your business path:`);
2966
2965
  if (proofPath !== undefined) {
2967
- console.log(`prove it OFFLINE first: npx githolon proof # the generated proof's offline legs on a LOCAL holon (no cloud workspace)`);
2968
- console.log(` inner loop: npx githolon dev # watch -> recompile -> law live locally -> offline proof, on every save`);
2969
- console.log(` then the cloud loop: npx githolon proof --live # throwaway workspace, retired on exit (--keep keeps it)`);
2966
+ console.log(` npx githolon proof # generated birth -> verify -> durable restore -> business read-back`);
2967
+ console.log(` npx githolon dev # rerun that proof on every save`);
2968
+ console.log(` npx githolon proof --live # the same business proof against Nomos Cloud`);
2970
2969
  if (proofDomains.length > 1) {
2971
2970
  console.log(` this package proves ${proofDomains.length} domains (${proofDomains.join(", ")}) — default '${proofDomains[0]}'; pick another: npx githolon proof --domain <key>`);
2972
2971
  }
2973
2972
  } else {
2974
- console.log(`prove it: npm run e2e # offline write -> sync -> admission -> cloud query, live`);
2973
+ console.log(` add a business birth declaration to generate an end-to-end proof`);
2975
2974
  }
2976
2975
 
2977
2976
  if (analyzeFlag) runDartAnalyze(dartOut);
@@ -80,6 +80,7 @@ import type { CombinedDecl } from "./combined.js";
80
80
  import type { InvariantBody, InvariantEvidence, InvariantVerdict } from "./relation.js";
81
81
  import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
82
82
  import type { QueryRow } from "./report.js";
83
+ import { expandBusinessBirthExports } from "./business_birth.js";
83
84
 
84
85
  /** One bundled domain module: a bag of named exports the entry scans by SHAPE. */
85
86
  export type DomainModuleExports = Record<string, unknown>;
@@ -244,9 +245,10 @@ function combinedsOf(mod: DomainModuleExports): CombinedDecl[] {
244
245
  }
245
246
 
246
247
  /** Spread-merge a domain key's ORDERED module list (later wins on collision). */
247
- function mergeModules(mods: readonly DomainModuleExports[]): DomainModuleExports {
248
+ function mergeModules(mods: readonly DomainModuleExports[], domain: string): DomainModuleExports {
248
249
  let merged: DomainModuleExports = {};
249
250
  for (const m of mods) merged = { ...merged, ...m };
251
+ merged = expandBusinessBirthExports(merged, domain);
250
252
  // `capability()` bundles expand into discoverable entries here — the engine
251
253
  // plane's ONE merge point, so the registry sees the task aggregate + quartet
252
254
  // without per-piece re-exports (capability_exports.ts).
@@ -465,7 +467,7 @@ export function collectEngineRouting(
465
467
  if (!list.includes(domainName)) list.push(domainName);
466
468
  };
467
469
  for (const [domainName, mods] of Object.entries(domains)) {
468
- const slice = buildDomainSlice(mergeModules(mods));
470
+ const slice = buildDomainSlice(mergeModules(mods, domainName));
469
471
  // Type-keyed dispatches arrive with the ROW's type — QUALIFIED for a namespaced aggregate (the
470
472
  // runtime `__type` stamp), bare for a global one / a pre-namespacing row. Route BOTH keys so
471
473
  // either era resolves to the declaring domain (the slice lookup strips the qualifier back off).
@@ -551,7 +553,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
551
553
  const sources = config.domains[domainName];
552
554
  if (sources === undefined) return undefined;
553
555
  const forced = sources.map((m) => (typeof m === "function" ? m() : m));
554
- const slice = buildDomainSlice(mergeModules(forced));
556
+ const slice = buildDomainSlice(mergeModules(forced, domainName));
555
557
  sliceCache.set(domainName, slice);
556
558
  return slice;
557
559
  }
@@ -943,7 +945,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
943
945
  for (const domainName of domainNames) {
944
946
  const sources = config.domains[domainName];
945
947
  if (sources === undefined) continue;
946
- const merged = mergeModules(sources.map((m) => (typeof m === "function" ? m() : m)));
948
+ const merged = mergeModules(sources.map((m) => (typeof m === "function" ? m() : m)), domainName);
947
949
  // Aggregates dedup by wire id + RelationTuple auto-ensure (the compose contract) — openapi
948
950
  // reads aggregates only for the hasMany inverse indexes, so this matches composeDomainModule.
949
951
  const byId = aggregatesOf(merged);
package/src/index.ts CHANGED
@@ -221,6 +221,8 @@ export {
221
221
  birthInstance,
222
222
  type BirthRecipe,
223
223
  type GenesisChainOrBuilder,
224
+ type BusinessBirthDecl,
225
+ type BusinessBirthSpec,
224
226
  } from "./births.js";
225
227
  // THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain): a home/child law exports
226
228
  // `requireParentAttestedBirth` to declare it requires a parent-attested-birth cert chain (the
package/src/law_check.ts CHANGED
@@ -371,6 +371,9 @@ function planReturnFindings(domain: string, dir: Directive<unknown>, probe: Prob
371
371
  // Framework-consumed convention fields (auto-stamped `…At`, birth subject/time) are excluded.
372
372
  function unreadPayloadFindings(domain: string, dir: Directive<unknown>, probe: ProbeResult): Finding[] {
373
373
  if (probe.threw) return [];
374
+ // A business birth's public fields are consumed by its hidden child-seed directive. That seed is linted
375
+ // independently; warning again on the parent half is a false positive caused by the lowering boundary.
376
+ if ((dir as { businessBirth?: unknown }).businessBirth !== undefined) return [];
374
377
  const stamped = new Set(autoStampFields(dir.payloadSchema as z.ZodTypeAny));
375
378
  // The GATE reads some payload fields the PLAN never does: `.requires(rel, { objectFrom })` derives
376
379
  // the gate object from a named payload id field (per-record authz). Excluding it keeps a real
@@ -379,6 +382,14 @@ function unreadPayloadFindings(domain: string, dir: Directive<unknown>, probe: P
379
382
  const objectFromField = (dir as { requiresRelationGate?: { objectFrom?: { idField?: string } } })
380
383
  .requiresRelationGate?.objectFrom?.idField;
381
384
  if (typeof objectFromField === "string") gateConsumed.add(objectFromField);
385
+ const firstDeviceGate = (dir as {
386
+ firstDeviceEnrollGate?: { tokenField: string; keyHashField: string; verifiedViaField: string };
387
+ }).firstDeviceEnrollGate;
388
+ if (firstDeviceGate !== undefined) {
389
+ gateConsumed.add(firstDeviceGate.tokenField);
390
+ gateConsumed.add(firstDeviceGate.keyHashField);
391
+ gateConsumed.add(firstDeviceGate.verifiedViaField);
392
+ }
382
393
  // A dynamic .readsFrom(fromPayload("<wsField>"), …) edge routes the foreign read by a payload field the
383
394
  // ROUTING lane consumes (the plan may pass it straight to read(…, {from}) without a tracked property
384
395
  // read on the sampled path) — a declared trust-edge field is never a dead field.