@githolon/dsl 0.100.5 → 0.100.6

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.
@@ -42,6 +42,44 @@ import {
42
42
  retireChildRequiresBirth,
43
43
  retireChildNotReclaimed,
44
44
  } from "./framework/birth_meter.js";
45
+ import { RelationTuple, relationTupleInstanceId, relationTuplesByObject } from "./framework/relations.js";
46
+ import { GENESIS_WARRANT_ID, GenesisWarrant, seedGenesis } from "./framework/genesis.js";
47
+ import {
48
+ BIRTH_ATTESTATION_ID,
49
+ BirthAttestation,
50
+ birthAttestation,
51
+ seedBirthAttestation,
52
+ } from "./framework/home.js";
53
+ import { requireParentAttestedBirth } from "./births.js";
54
+ import {
55
+ SignerDelegation,
56
+ enrollDelegation,
57
+ enrollSigner,
58
+ identityRelations,
59
+ signerDelegationByUser,
60
+ signerObject,
61
+ } from "./framework/signing_identity.js";
62
+ import { AuthProvider, authProvider, setAuthProvider } from "./framework/auth_provider_law.js";
63
+ import { refAs, workspaceInvariant } from "./framework/workspace_invariant.js";
64
+
65
+ /** Lightweight copy of the framework's founding-signer guard, kept here to avoid importing the whole
66
+ * governance module (whose unrelated captured-read laws would otherwise survive into every child lump). */
67
+ const bornFounderSignerWriteOnce = workspaceInvariant("FounderSignerWriteOnce")
68
+ .on("enrollSigner")
69
+ .reads((ctx) => [
70
+ refAs(
71
+ "tuple",
72
+ "RelationTuple",
73
+ "pre:" + relationTupleInstanceId(signerObject(String((ctx.intent ?? {}).principal ?? "")), "signer"),
74
+ ),
75
+ ])
76
+ .assert(({ tuple }, ctx) => {
77
+ const subjects = (tuple as { subjects?: Record<string, unknown> })?.subjects ?? {};
78
+ const want = "key:" + String((ctx?.intent ?? {}).keyHash ?? "");
79
+ return Object.keys(subjects).every((key) => key === want)
80
+ ? { accept: true }
81
+ : { reject: "founding-signer-already-enrolled" };
82
+ });
45
83
 
46
84
  /**
47
85
  * The sanctioning declaration — an `ensures(NomosBirthRecord)` directive whose plan emits ZERO ops.
@@ -70,36 +108,126 @@ export function withBirthMeterExports<M extends Record<string, unknown>>(mod: M)
70
108
  (v as { birthsChild?: boolean }).birthsChild === true &&
71
109
  typeof (v as Directive<unknown>).plan === "function",
72
110
  );
73
- if (!declaresBirth) return mod;
111
+ // A law that accepts the kernel-injected birth attestation is itself a possible CHILD even if it never
112
+ // births anything. Such a law must also accept the framework-owned founding signer step, otherwise the
113
+ // child can replay but can never sign its first write or birth a descendant without tenant recipe work.
114
+ const acceptsBirth = Object.values(mod).some(
115
+ (v) =>
116
+ v !== null &&
117
+ typeof v === "object" &&
118
+ (v as { id?: unknown }).id === "seedBirthAttestation" &&
119
+ typeof (v as Directive<unknown>).plan === "function",
120
+ );
121
+ if (!declaresBirth && !acceptsBirth) return mod;
74
122
  const out: Record<string, unknown> = { ...mod };
75
- // Business birth hides framework maintenance declarations from the generated application facade while
76
- // retaining them in certified law for the operator's structural offer discovery. A non-business
77
- // `.births()` law keeps its existing explicit generated surface.
78
- const businessInternal = Object.values(mod).some(
79
- (v) => v !== null && typeof v === "object" && (v as { businessBirth?: unknown }).businessBirth !== undefined,
123
+ const internal = <T extends object>(v: T): T => ({ ...v, businessBirthInternal: true }) as T;
124
+ const hasOwn = (key: string): boolean => Object.prototype.hasOwnProperty.call(out, key);
125
+ const isGeneratedInternal = (value: unknown): boolean =>
126
+ value !== null &&
127
+ typeof value === "object" &&
128
+ (value as { businessBirthInternal?: unknown }).businessBirthInternal === true;
129
+ const reservedNameError = (key: string): Error => new Error(
130
+ `Nomos internal export name '${key}' is reserved inside a birth-capable law, but the tenant module ` +
131
+ "already exports a different concept there. Nomos will not silently overwrite or reroute it; rename " +
132
+ "the tenant export.",
80
133
  );
81
- const presented = <T extends object>(v: T): T =>
82
- businessInternal ? ({ ...v, businessBirthInternal: true } as T) : v;
83
- if (!("nomosBirthRecordEnsure" in out)) out["nomosBirthRecordEnsure"] = presented(nomosBirthRecordEnsure);
84
- if (!("NomosBirthRecord" in out)) out["NomosBirthRecord"] = presented(NomosBirthRecord);
85
- if (!("NomosChildLifecycle" in out)) out["NomosChildLifecycle"] = presented(NomosChildLifecycle);
86
- if (!("BirthPolicy" in out)) out["BirthPolicy"] = presented(BirthPolicy);
87
- if (!("BirthGrant" in out)) out["BirthGrant"] = presented(BirthGrant);
88
- if (!("retireChild" in out)) out["retireChild"] = presented(retireChild);
89
- if (!("reclaimChild" in out)) out["reclaimChild"] = presented(reclaimChild);
90
- if (!("retireChildAsAdmin" in out)) out["retireChildAsAdmin"] = presented(retireChildAsAdmin);
91
- if (!("reclaimChildAsAdmin" in out)) out["reclaimChildAsAdmin"] = presented(reclaimChildAsAdmin);
92
- if (!("reclaimChildRequiresRetired" in out)) out["reclaimChildRequiresRetired"] = reclaimChildRequiresRetired;
93
- if (!("reclaimChildAsAdminRequiresRetired" in out)) {
94
- out["reclaimChildAsAdminRequiresRetired"] = reclaimChildAsAdminRequiresRetired;
95
- }
96
- if (!("retireChildRequiresBirth" in out)) out["retireChildRequiresBirth"] = retireChildRequiresBirth;
97
- if (!("retireChildNotReclaimed" in out)) out["retireChildNotReclaimed"] = retireChildNotReclaimed;
98
- if (!("retireChildAsAdminRequiresBirth" in out)) {
99
- out["retireChildAsAdminRequiresBirth"] = retireChildAsAdminRequiresBirth;
100
- }
101
- if (!("retireChildAsAdminNotReclaimed" in out)) {
102
- out["retireChildAsAdminNotReclaimed"] = retireChildAsAdminNotReclaimed;
103
- }
134
+ /** Reserve a framework-only JS export slot as well as its stable law id. The keys are deliberately obscure,
135
+ * but obscurity is collision avoidance, not collision handling: an occupied slot is always a loud refusal. */
136
+ const reserve = <T>(key: string, canonical: T): void => {
137
+ if (!hasOwn(key)) {
138
+ out[key] = canonical;
139
+ return;
140
+ }
141
+ const existing = out[key];
142
+ if (existing !== canonical && !isGeneratedInternal(existing)) throw reservedNameError(key);
143
+ };
144
+ /**
145
+ * Framework ownership follows the stable law id, not the tenant's export key. Older laws commonly
146
+ * re-exported `enrollSigner`/`seedGenesis` under their public names; merely adding a second hidden
147
+ * `__nomos_*` binding leaves BOTH declarations discoverable and lets codegen expose the old one. Mark
148
+ * every matching binding internal, and add the canonical declaration only when the law carried none.
149
+ */
150
+ const internalize = <T extends { readonly id: string }>(key: string, canonical: T): void => {
151
+ if (hasOwn(key)) {
152
+ const existing = out[key];
153
+ const sameGeneratedId =
154
+ isGeneratedInternal(existing) &&
155
+ (existing as { id?: unknown }).id === canonical.id;
156
+ if (existing !== canonical && !sameGeneratedId) throw reservedNameError(key);
157
+ }
158
+ let found = false;
159
+ for (const [exportName, value] of Object.entries(out)) {
160
+ if (
161
+ value !== null &&
162
+ typeof value === "object" &&
163
+ (value as { id?: unknown }).id === canonical.id
164
+ ) {
165
+ const isCanonicalSingleton = value === canonical;
166
+ const isNomosGeneratedBinding =
167
+ (value as { businessBirthInternal?: unknown }).businessBirthInternal === true;
168
+ if (!isCanonicalSingleton && !isNomosGeneratedBinding) {
169
+ throw new Error(
170
+ `Nomos framework id '${canonical.id}' is reserved inside a birth-capable law, but export ` +
171
+ `'${exportName}' declares a different tenant-owned concept with that id. Nomos will not ` +
172
+ `silently reinterpret it as framework machinery; rename the tenant concept.`,
173
+ );
174
+ }
175
+ out[exportName] = internal(value);
176
+ found = true;
177
+ }
178
+ }
179
+ if (!found) out[key] = internal(canonical);
180
+ };
181
+ // THE BORN-WORKSPACE CORE: the kernel appends enrollSigner -> attestation -> warrant to every valid birth.
182
+ // Carry the law needed to judge that internal step in every possible child, plus the write-once guard that
183
+ // keeps the founding lane a genesis primitive. These declarations are certified but never tenant-facing.
184
+ internalize("__nomos_RelationTuple", RelationTuple);
185
+ reserve("__nomos_identityRelations", identityRelations);
186
+ internalize("__nomos_enrollSigner", enrollSigner);
187
+ internalize("__nomos_relationTuplesByObject", relationTuplesByObject);
188
+ internalize("__nomos_GenesisWarrant", GenesisWarrant);
189
+ internalize("__nomos_seedGenesis", seedGenesis);
190
+ internalize("__nomos_BirthAttestation", BirthAttestation);
191
+ internalize("__nomos_seedBirthAttestation", seedBirthAttestation);
192
+ internalize("__nomos_birthAttestation", birthAttestation);
193
+ internalize("__nomos_AuthProvider", AuthProvider);
194
+ internalize("__nomos_setAuthProvider", setAuthProvider);
195
+ internalize("__nomos_authProvider", authProvider);
196
+ reserve("__nomos_founderSignerWriteOnce", bornFounderSignerWriteOnce);
197
+ if (!declaresBirth) return out as M;
198
+ // Birth lifecycle is Nomos machinery for every birth-capable law. It remains certified/routable law,
199
+ // but no generated tenant API gets a second maintenance lane beside business `birth(...)`.
200
+ internalize("__nomos_nomosBirthRecordEnsure", nomosBirthRecordEnsure);
201
+ internalize("__nomos_NomosBirthRecord", NomosBirthRecord);
202
+ internalize("__nomos_NomosChildLifecycle", NomosChildLifecycle);
203
+ internalize("__nomos_BirthPolicy", BirthPolicy);
204
+ internalize("__nomos_BirthGrant", BirthGrant);
205
+ internalize("__nomos_retireChild", retireChild);
206
+ internalize("__nomos_reclaimChild", reclaimChild);
207
+ internalize("__nomos_retireChildAsAdmin", retireChildAsAdmin);
208
+ internalize("__nomos_reclaimChildAsAdmin", reclaimChildAsAdmin);
209
+ reserve("__nomos_reclaimChildRequiresRetired", reclaimChildRequiresRetired);
210
+ reserve("__nomos_reclaimChildAsAdminRequiresRetired", reclaimChildAsAdminRequiresRetired);
211
+ reserve("__nomos_retireChildRequiresBirth", retireChildRequiresBirth);
212
+ reserve("__nomos_retireChildNotReclaimed", retireChildNotReclaimed);
213
+ reserve("__nomos_retireChildAsAdminRequiresBirth", retireChildAsAdminRequiresBirth);
214
+ reserve("__nomos_retireChildAsAdminNotReclaimed", retireChildAsAdminNotReclaimed);
215
+ // ONE BIRTH LANE ALSO MEANS ONE PRIVATE SIGNING LANE. The client completes a `.births()` offer's signer
216
+ // enrollment, delegation and cert retry internally; therefore every birth-declaring parent law must carry
217
+ // the declarations that make those framework writes lawful. Previously only the newer `businessBirth`
218
+ // helper composed them, while an ordinary `.births()` directive forced the harness/app to understand
219
+ // `enrollDelegation` (or failed as an unverifiable black box). Auto-compose the exact same framework
220
+ // declarations for ALL births and mark them presentation-internal: they remain certified/routable law,
221
+ // but never appear in TS/Dart/OpenAPI tenant surfaces.
222
+ internalize("__nomos_SignerDelegation", SignerDelegation);
223
+ internalize("__nomos_enrollDelegation", enrollDelegation);
224
+ internalize("__nomos_signerDelegationByUser", signerDelegationByUser);
225
+ reserve("__nomos_parentAttestedBirth", internal({
226
+ ...requireParentAttestedBirth,
227
+ ownerObject: GENESIS_WARRANT_ID,
228
+ ownerField: "principal",
229
+ certObject: BIRTH_ATTESTATION_ID,
230
+ certField: "certChain",
231
+ }));
104
232
  return out as M;
105
233
  }
@@ -75,6 +75,7 @@ import { collectValueObjectDescriptors } from "./value_object.js";
75
75
  import { emitUsd } from "./usd.js";
76
76
  import { expandCapabilityExports } from "./capability_exports.js";
77
77
  import { RelationTuple, relationTuplesByObject } from "./framework/relations.js";
78
+ import { signerDelegationByUser } from "./framework/signing_identity.js";
78
79
  import { AuthProvider, setAuthProvider, authProvider } from "./framework/workspaces.js";
79
80
  import { parentAttestedBirthMarker } from "./births.js";
80
81
  import { discoverUnsanctionedEnsures, sanctioningEnsureDirective } from "./secondary_ensure.js";
@@ -125,7 +126,10 @@ export function contractsOf(mod: Mod): import("./contract.js").ContractHandle[]
125
126
 
126
127
  /** Collect every exported `Directive` ({id, plan, aggregateId} duck-type). */
127
128
  export function directivesOf(mod: Mod): Directive<any>[] {
128
- const out: Directive<any>[] = [];
129
+ // One wire directive id is one law declaration. Merge seams may retain both an old tenant re-export and
130
+ // Nomos's presentation-internal wrapper under different JS keys; later wins so the framework wrapper
131
+ // controls codegen without duplicating the hashed/routable law.
132
+ const byId = new Map<string, Directive<any>>();
129
133
  for (const v of Object.values(mod)) {
130
134
  if (
131
135
  v &&
@@ -134,10 +138,10 @@ export function directivesOf(mod: Mod): Directive<any>[] {
134
138
  typeof (v as { plan?: unknown }).plan === "function" &&
135
139
  typeof (v as { aggregateId?: unknown }).aggregateId === "string"
136
140
  ) {
137
- out.push(v as Directive<any>);
141
+ byId.set((v as { id: string }).id, v as Directive<any>);
138
142
  }
139
143
  }
140
- return out;
144
+ return [...byId.values()];
141
145
  }
142
146
 
143
147
  /** The NAMED-KEYS spec for one engine domain (the serialization of `module(...)`). */
@@ -306,7 +310,11 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
306
310
  }
307
311
  const readModels = spec.readModelAggregates ?? aggregates.map((a) => a.id);
308
312
 
309
- const queries = [...(spec.queries ?? [])];
313
+ // As with directives, one stable query id is one declaration. This also makes an internal framework
314
+ // wrapper win over an older public re-export discovered earlier in the module bag.
315
+ const queryById = new Map<string, QueryDecl>();
316
+ for (const query of spec.queries ?? []) queryById.set(query.id, query);
317
+ const queries = [...queryById.values()];
310
318
  const counts = [...(spec.counts ?? [])];
311
319
  const spatials = [...(spec.spatials ?? [])];
312
320
  const deriveds = [...(spec.deriveds ?? [])];
@@ -316,7 +324,23 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
316
324
  const mins = [...(spec.mins ?? [])];
317
325
  const maxes = [...(spec.maxes ?? [])];
318
326
  const workspaceTypes = [...(spec.workspaceTypes ?? [])];
319
- const workspaceInvariants = [...(spec.workspaceInvariants ?? [])];
327
+ // Invariants exported by framework auto-composition must enter the manifest at this same seam; callers of
328
+ // composeDomainModule (tests and package builders alike) should not need to rediscover/rethread them.
329
+ const workspaceInvariantById = new Map<string, WorkspaceInvariantDecl>();
330
+ for (const value of Object.values(merged)) {
331
+ if (
332
+ value !== null &&
333
+ typeof value === "object" &&
334
+ (value as { __isWorkspaceInvariant?: unknown }).__isWorkspaceInvariant === true
335
+ ) {
336
+ const invariant = value as WorkspaceInvariantDecl;
337
+ workspaceInvariantById.set(invariant.id, invariant);
338
+ }
339
+ }
340
+ for (const invariant of spec.workspaceInvariants ?? []) {
341
+ workspaceInvariantById.set(invariant.id, invariant);
342
+ }
343
+ const workspaceInvariants = [...workspaceInvariantById.values()];
320
344
  if (declaresBirth) {
321
345
  const haveInvariant = new Set(workspaceInvariants.map((w) => w.id));
322
346
  for (const lifecycle of [
@@ -343,10 +367,21 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
343
367
  if (declaresBirth) {
344
368
  const haveQuery = new Set(queries.map((q) => q.id));
345
369
  for (const q of BIRTH_FRAMEWORK_QUERIES) {
346
- if (!haveQuery.has(q.id)) queries.push(q);
370
+ if (!haveQuery.has(q.id)) {
371
+ queries.push({ ...q, businessBirthInternal: true } as QueryDecl);
372
+ }
347
373
  }
348
374
  // The inheritance read (`.reads("authProvider")` on birthHome) routes like any declared query.
349
375
  if (!haveQuery.has(authProvider.id)) queries.push(authProvider);
376
+ // The client's private birth ceremony checks the actor's delegation before creating it. Carry the
377
+ // structural read in certified routing law for every `.births()` domain, but keep it off tenant codegen.
378
+ if (!haveQuery.has(signerDelegationByUser.id)) {
379
+ const internalDelegationQuery: QueryDecl & { readonly businessBirthInternal: true } = {
380
+ ...signerDelegationByUser,
381
+ businessBirthInternal: true,
382
+ };
383
+ queries.push(internalDelegationQuery);
384
+ }
350
385
  }
351
386
  const relationTypes = [...(spec.relationTypes ?? [])];
352
387
  // RELATION-USING LAW AUTO-COMPOSES ITS AUDIT READ (co2 recovery DX — same idiom as the meter
@@ -229,7 +229,7 @@ function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unkno
229
229
  __nomos_birthAttestation: internalSurface(birthAttestation),
230
230
  __nomos_RelationTuple: internalSurface(RelationTuple),
231
231
  __nomos_relationTuplesByObject: internalSurface(relationTuplesByObject),
232
- __nomos_identityRelations: identityRelations,
232
+ __nomos_identityRelations: internalSurface(identityRelations),
233
233
  __nomos_enrollSigner: internalDirective(enrollSigner),
234
234
  __nomos_enrollFirstDevice: internalDirective(enrollFirstDevice),
235
235
  __nomos_SignerDelegation: internalSurface(SignerDelegation),
@@ -238,19 +238,19 @@ function lowerOne(decl: BusinessBirthDecl, domain: string): Record<string, unkno
238
238
  __nomos_AuthProvider: internalSurface(AuthProvider),
239
239
  __nomos_setAuthProvider: internalDirective(setAuthProvider),
240
240
  __nomos_authProvider: internalSurface(authProvider),
241
- // Exact export keys prevent the generic `.births()` auto-compose seam from inserting a second,
242
- // unmarked copy. The values are still the canonical framework declarations.
243
- NomosBirthRecord: internalSurface(NomosBirthRecord),
244
- NomosChildLifecycle: internalSurface(NomosChildLifecycle),
245
- BirthPolicy: internalSurface(BirthPolicy),
246
- BirthGrant: internalSurface(BirthGrant),
247
- nomosBirthRecordEnsure: internalDirective(nomosBirthRecordEnsure),
241
+ // Obscure framework export keys minimize accidental tenant collisions. Stable law ids remain the real
242
+ // identity; the generic composition seam rejects a tenant-owned declaration with any of those ids.
243
+ __nomos_NomosBirthRecord: internalSurface(NomosBirthRecord),
244
+ __nomos_NomosChildLifecycle: internalSurface(NomosChildLifecycle),
245
+ __nomos_BirthPolicy: internalSurface(BirthPolicy),
246
+ __nomos_BirthGrant: internalSurface(BirthGrant),
247
+ __nomos_nomosBirthRecordEnsure: internalDirective(nomosBirthRecordEnsure),
248
248
  __nomos_birthRecordsByPrincipal: internalSurface(birthRecordsByPrincipal),
249
249
  __nomos_childLifecyclesByPrincipal: internalSurface(childLifecyclesByPrincipal),
250
250
  __nomos_childLifecyclesByStatus: internalSurface(childLifecyclesByStatus),
251
251
  __nomos_birthPolicy: internalSurface(birthPolicy),
252
252
  __nomos_birthGrantByPrincipal: internalSurface(birthGrantByPrincipal),
253
- __nomos_parentAttestedBirth: businessParentAttestedBirth,
253
+ __nomos_parentAttestedBirth: internalSurface(businessParentAttestedBirth),
254
254
  };
255
255
  }
256
256
 
@@ -268,6 +268,28 @@ export function expandBusinessBirthExports<M extends Record<string, unknown>>(
268
268
  );
269
269
  }
270
270
  const lowered = lowerOne(declarations[0]!, domain);
271
+ for (const [key, generated] of Object.entries(lowered)) {
272
+ if (!Object.prototype.hasOwnProperty.call(mod, key)) continue;
273
+ const existing = mod[key];
274
+ const sameGeneratedDeclaration =
275
+ existing !== null &&
276
+ typeof existing === "object" &&
277
+ generated !== null &&
278
+ typeof generated === "object" &&
279
+ (((existing as { businessBirthInternal?: unknown }).businessBirthInternal === true &&
280
+ (generated as { businessBirthInternal?: unknown }).businessBirthInternal === true &&
281
+ (existing as { id?: unknown }).id === (generated as { id?: unknown }).id) ||
282
+ ((existing as { businessBirth?: unknown }).businessBirth !== undefined &&
283
+ (generated as { businessBirth?: unknown }).businessBirth !== undefined &&
284
+ (existing as { id?: unknown }).id === (generated as { id?: unknown }).id));
285
+ if (!sameGeneratedDeclaration) {
286
+ throw new Error(
287
+ `Nomos internal export name '${key}' is reserved by birth(${declarations[0]!.aggregate.id}), but ` +
288
+ "the tenant module already exports a different concept there. Nomos will not silently overwrite " +
289
+ "or reroute it; rename the tenant export.",
290
+ );
291
+ }
292
+ }
271
293
  return { ...mod, ...lowered };
272
294
  }
273
295
 
@@ -939,13 +939,17 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
939
939
  ` if (b is NomosSession) await b.assertCompatible(interface);`,
940
940
  ` }`,
941
941
  ``,
942
- ` /// THE ONE DISPATCH LANE every generated directive method routes through: lazy preflight, then`,
943
- ` /// the session's offer (the generated-internal use of the engine-level lane).`,
944
- ` Future<NomosOfferOutcome> _offer(String directiveId, Object? payload, {List<Map<String, Object?>>? attestations}) async {`,
945
- ` await _preflightOnce();`,
946
- ` return bridge.offerOutcome(domain: ${dartStr(domain)}, directiveId: directiveId, payload: payload, actor: actor, domainHash: domainHash, attestations: attestations);`,
947
- ` }`,
948
- ``,
942
+ ...(dirs.length > 0 || birthDirs.some((d) => (d as { businessBirth?: unknown }).businessBirth !== undefined)
943
+ ? [
944
+ ` /// THE ONE DISPATCH LANE every generated directive method routes through: lazy preflight, then`,
945
+ ` /// the session's offer (the generated-internal use of the engine-level lane).`,
946
+ ` Future<NomosOfferOutcome> _offer(String directiveId, Object? payload, {List<Map<String, Object?>>? attestations}) async {`,
947
+ ` await _preflightOnce();`,
948
+ ` return bridge.offerOutcome(domain: ${dartStr(domain)}, directiveId: directiveId, payload: payload, actor: actor, domainHash: domainHash, attestations: attestations);`,
949
+ ` }`,
950
+ ``,
951
+ ]
952
+ : []),
949
953
  ...(encPresent ? dartEncClassMembers(encMap!) : []),
950
954
  // THE OWNER CEREMONY (slice 2): grantAndWrap / revokeAndRotate / rotate — the operator-blind key-epoch
951
955
  // handshake as one call. Bind `actor` (the owner) to author the fan-out. Only for a domain with a scope.
@@ -598,10 +598,13 @@ function collectDartSharedSymbols(
598
598
  const frameworkAggById = new Map<string, AggregateHandle>();
599
599
  const frameworkDirById = new Map<string, ModDirective>();
600
600
  const frameworkQueryById = new Map<string, ModQuery>();
601
+ const isInternal = (value: unknown): boolean =>
602
+ !!value && typeof value === "object"
603
+ && (value as { businessBirthInternal?: unknown }).businessBirthInternal === true;
601
604
  for (const mod of modules) {
602
- for (const a of mod.aggregates) if (isFrameworkAggregate(a)) frameworkAggById.set(a.id, a);
603
- for (const d of mod.directives) if (isFrameworkDirective(d)) frameworkDirById.set(d.id, d);
604
- for (const q of mod.queries ?? []) if (isFrameworkQuery(q)) frameworkQueryById.set(q.id, q);
605
+ for (const a of mod.aggregates) if (!isInternal(a) && isFrameworkAggregate(a)) frameworkAggById.set(a.id, a);
606
+ for (const d of mod.directives) if (!isInternal(d) && isFrameworkDirective(d)) frameworkDirById.set(d.id, d);
607
+ for (const q of mod.queries ?? []) if (!isInternal(q) && isFrameworkQuery(q)) frameworkQueryById.set(q.id, q);
605
608
  }
606
609
  const frameworkAggs = [...frameworkAggById.values()];
607
610
  const frameworkDirs = [...frameworkDirById.values()];
@@ -1773,7 +1776,9 @@ async function main(): Promise<void> {
1773
1776
  if (directivesOf(merged).some((dir) => (dir as { birthsChild?: boolean }).birthsChild === true)) {
1774
1777
  const haveQuery = new Set(queries.map((q) => q.id));
1775
1778
  for (const q of BIRTH_FRAMEWORK_QUERIES) {
1776
- if (!haveQuery.has(q.id)) queries.push(q);
1779
+ if (!haveQuery.has(q.id)) {
1780
+ queries.push({ ...q, businessBirthInternal: true } as QueryDecl);
1781
+ }
1777
1782
  }
1778
1783
  }
1779
1784
  // RELATION-USING LAW AUTO-COMPOSES ITS AUDIT READ (co2 recovery DX, same idiom as the birth-meter
@@ -2583,12 +2588,16 @@ async function main(): Promise<void> {
2583
2588
  // the read overlay; the workspace's full spec is the runtime merge of installed packages' specs
2584
2589
  // (currentLaw); the host ingests it and builds its router from it — no domain/directive name in host
2585
2590
  // source. Self-describing for the whole web ecosystem (Swagger/codegen/Hono/agent tool-calling).
2586
- const openapi = mergeOpenApiFragments(
2591
+ const publicOpenapi = mergeOpenApiFragments(
2587
2592
  domainModules.map((m) => generateOpenApiFragment(m, { lawHash: domainHash })),
2588
2593
  { title: `${cfg.name} (nomos package)`, lawHash: domainHash },
2589
2594
  );
2595
+ const runtimeOpenapi = mergeOpenApiFragments(
2596
+ domainModules.map((m) => generateOpenApiFragment(m, { lawHash: domainHash, includeFrameworkRoutes: true })),
2597
+ { title: `${cfg.name} (nomos package)`, lawHash: domainHash },
2598
+ );
2590
2599
  const openapiPath = path.join(outDir, `${cfg.name}.openapi.json`);
2591
- writeFileSync(openapiPath, JSON.stringify(openapi, null, 2) + "\n", "utf8");
2600
+ writeFileSync(openapiPath, JSON.stringify(publicOpenapi, null, 2) + "\n", "utf8");
2592
2601
 
2593
2602
  // ── 4. the one-call deploy body (package only) ──
2594
2603
  // SELF-DESCRIBING: `summary` states what the (otherwise hex-opaque) package
@@ -2620,7 +2629,7 @@ async function main(): Promise<void> {
2620
2629
  interface: packageInterface,
2621
2630
  // THE SELF-DESCRIBING API: this package's OpenAPI spec (law-derived). The worker stages it on the
2622
2631
  // manifest overlay; a read serves the workspace's merged spec; the host builds its router from it.
2623
- openapi,
2632
+ openapi: runtimeOpenapi,
2624
2633
  // THE CLIENT/LAW COMPAT MANIFEST (§5): a DIAGNOSIS surface (law hash + interface hash + generator
2625
2634
  // provenance) the connect-time classifier reads. Frontend-only — the package bytes above ignore it.
2626
2635
  nomosCompat,
@@ -2895,7 +2904,7 @@ async function main(): Promise<void> {
2895
2904
  }
2896
2905
  console.log(` client ${rel(clientPath)} (typed TS client — payloads, read models, query accessors)`);
2897
2906
  for (const note of recursiveNotes) console.log(` note ${note}`);
2898
- console.log(` openapi ${rel(openapiPath)} (${Object.keys(openapi.paths).length} operation(s) — the law's self-describing API; the host's runtime routing table)`);
2907
+ console.log(` openapi ${rel(openapiPath)} (${Object.keys(publicOpenapi.paths).length} operation(s) — the public business API; private framework routes ride only in the deploy manifest)`);
2899
2908
  if (proofPath !== undefined) {
2900
2909
  console.log(` proof ${rel(proofPath)} (a runnable e2e GENERATED from your law — run: githolon proof)`);
2901
2910
  } else {
@@ -123,6 +123,10 @@ function aggFieldReadKind(f: Field): string {
123
123
  * stable-id surface. Without stable ids (pre-#58) aggregate fields key by NAME and the
124
124
  * kind comes from the field declaration — a graceful, rename-unaware fallback. */
125
125
  export function domainInterface(mod: DomainModule, stableIds?: StableIds): DomainInterface {
126
+ const isInternal = (value: unknown): boolean =>
127
+ !!value &&
128
+ typeof value === "object" &&
129
+ (value as { businessBirthInternal?: unknown }).businessBirthInternal === true;
126
130
  const enumTypes = new Map<string, string>();
127
131
  // DETERMINISTIC KEY ORDER — directive ids, aggregate sids, and field keys are SORTED, so the
128
132
  // emitted interface is byte-stable and the layered-USD fold (which merges + sorts) reproduces it
@@ -134,21 +138,27 @@ export function domainInterface(mod: DomainModule, stableIds?: StableIds): Domai
134
138
  };
135
139
  const directives: Record<string, DirectivePayloadShape> = {};
136
140
  const dirEntries: Array<[string, DirectivePayloadShape]> = [];
137
- for (const d of (mod.directives ?? []) as Array<{ id?: unknown; payloadSchema?: unknown }>) {
141
+ for (const d of (mod.directives ?? []) as Array<{
142
+ id?: unknown;
143
+ payloadSchema?: unknown;
144
+ businessBirth?: { publicPayloadSchema?: unknown };
145
+ }>) {
138
146
  if (!d || typeof d.id !== "string") continue;
147
+ if (isInternal(d)) continue;
139
148
  const fieldEntries: Array<[string, PayloadFieldShape]> = [];
140
- const schema = d.payloadSchema;
149
+ const schema = d.businessBirth?.publicPayloadSchema ?? d.payloadSchema;
141
150
  if (schema && typeof schema === "object") {
142
151
  for (const spec of introspectPayload(schema as z.ZodTypeAny, enumTypes)) {
143
152
  fieldEntries.push([spec.name, { kind: payloadFieldKind(spec), optional: !!spec.optional }]);
144
153
  }
145
154
  }
146
- dirEntries.push([d.id, { fields: sortedRec(fieldEntries) }]);
155
+ dirEntries.push([d.businessBirth === undefined ? d.id : "birth", { fields: sortedRec(fieldEntries) }]);
147
156
  }
148
157
  Object.assign(directives, sortedRec(dirEntries));
149
158
  const aggregates: Record<string, AggregateReadShape> = {};
150
159
  const aggEntries: Array<[string, AggregateReadShape]> = [];
151
160
  for (const agg of (mod.aggregates ?? []) as Array<{ id: string; fields: Record<string, Field> }>) {
161
+ if (isInternal(agg)) continue;
152
162
  const label = agg.id;
153
163
  const sa = stableIds?.aggregates?.[label];
154
164
  const aggSid = sa?.sid ?? label;
@@ -168,6 +178,7 @@ export function domainInterface(mod: DomainModule, stableIds?: StableIds): Domai
168
178
  const foreignReadEntries: Array<[string, ForeignEdge[]]> = [];
169
179
  for (const d of (mod.directives ?? []) as Array<{ id?: unknown; declaredForeignReads?: ForeignEdge[] }>) {
170
180
  if (!d || typeof d.id !== "string") continue;
181
+ if (isInternal(d)) continue;
171
182
  if (d.declaredForeignReads !== undefined && d.declaredForeignReads.length > 0) {
172
183
  foreignReadEntries.push([d.id, d.declaredForeignReads.map((e) => ({ ...e }))]);
173
184
  }