@githolon/dsl 0.100.4 → 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.
@@ -32,11 +32,54 @@ import {
32
32
  } from "./birth_meter.js";
33
33
  import {
34
34
  reclaimChild,
35
+ reclaimChildAsAdmin,
36
+ reclaimChildAsAdminRequiresRetired,
35
37
  reclaimChildRequiresRetired,
36
38
  retireChild,
39
+ retireChildAsAdmin,
40
+ retireChildAsAdminNotReclaimed,
41
+ retireChildAsAdminRequiresBirth,
37
42
  retireChildRequiresBirth,
38
43
  retireChildNotReclaimed,
39
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
+ });
40
83
 
41
84
  /**
42
85
  * The sanctioning declaration — an `ensures(NomosBirthRecord)` directive whose plan emits ZERO ops.
@@ -65,25 +108,126 @@ export function withBirthMeterExports<M extends Record<string, unknown>>(mod: M)
65
108
  (v as { birthsChild?: boolean }).birthsChild === true &&
66
109
  typeof (v as Directive<unknown>).plan === "function",
67
110
  );
68
- 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;
69
122
  const out: Record<string, unknown> = { ...mod };
70
- // Business birth hides framework maintenance declarations from the generated application facade while
71
- // retaining them in certified law for the operator's structural offer discovery. A non-business
72
- // `.births()` law keeps its existing explicit generated surface.
73
- const businessInternal = Object.values(mod).some(
74
- (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.",
75
133
  );
76
- const presented = <T extends object>(v: T): T =>
77
- businessInternal ? ({ ...v, businessBirthInternal: true } as T) : v;
78
- if (!("nomosBirthRecordEnsure" in out)) out["nomosBirthRecordEnsure"] = presented(nomosBirthRecordEnsure);
79
- if (!("NomosBirthRecord" in out)) out["NomosBirthRecord"] = presented(NomosBirthRecord);
80
- if (!("NomosChildLifecycle" in out)) out["NomosChildLifecycle"] = presented(NomosChildLifecycle);
81
- if (!("BirthPolicy" in out)) out["BirthPolicy"] = presented(BirthPolicy);
82
- if (!("BirthGrant" in out)) out["BirthGrant"] = presented(BirthGrant);
83
- if (!("retireChild" in out)) out["retireChild"] = presented(retireChild);
84
- if (!("reclaimChild" in out)) out["reclaimChild"] = presented(reclaimChild);
85
- if (!("reclaimChildRequiresRetired" in out)) out["reclaimChildRequiresRetired"] = reclaimChildRequiresRetired;
86
- if (!("retireChildRequiresBirth" in out)) out["retireChildRequiresBirth"] = retireChildRequiresBirth;
87
- if (!("retireChildNotReclaimed" in out)) out["retireChildNotReclaimed"] = retireChildNotReclaimed;
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
+ }));
88
232
  return out as M;
89
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";
@@ -82,7 +83,10 @@ import { assertIdOfLint } from "./idof_lint.js";
82
83
  import { assertSurfacedResolvers } from "./surfaced_lint.js";
83
84
  import { expandBusinessBirthExports } from "./business_birth.js";
84
85
  import {
86
+ reclaimChildAsAdminRequiresRetired,
85
87
  reclaimChildRequiresRetired,
88
+ retireChildAsAdminNotReclaimed,
89
+ retireChildAsAdminRequiresBirth,
86
90
  retireChildNotReclaimed,
87
91
  retireChildRequiresBirth,
88
92
  } from "./framework/birth_meter.js";
@@ -122,7 +126,10 @@ export function contractsOf(mod: Mod): import("./contract.js").ContractHandle[]
122
126
 
123
127
  /** Collect every exported `Directive` ({id, plan, aggregateId} duck-type). */
124
128
  export function directivesOf(mod: Mod): Directive<any>[] {
125
- 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>>();
126
133
  for (const v of Object.values(mod)) {
127
134
  if (
128
135
  v &&
@@ -131,10 +138,10 @@ export function directivesOf(mod: Mod): Directive<any>[] {
131
138
  typeof (v as { plan?: unknown }).plan === "function" &&
132
139
  typeof (v as { aggregateId?: unknown }).aggregateId === "string"
133
140
  ) {
134
- out.push(v as Directive<any>);
141
+ byId.set((v as { id: string }).id, v as Directive<any>);
135
142
  }
136
143
  }
137
- return out;
144
+ return [...byId.values()];
138
145
  }
139
146
 
140
147
  /** The NAMED-KEYS spec for one engine domain (the serialization of `module(...)`). */
@@ -303,7 +310,11 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
303
310
  }
304
311
  const readModels = spec.readModelAggregates ?? aggregates.map((a) => a.id);
305
312
 
306
- 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()];
307
318
  const counts = [...(spec.counts ?? [])];
308
319
  const spatials = [...(spec.spatials ?? [])];
309
320
  const deriveds = [...(spec.deriveds ?? [])];
@@ -313,10 +324,33 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
313
324
  const mins = [...(spec.mins ?? [])];
314
325
  const maxes = [...(spec.maxes ?? [])];
315
326
  const workspaceTypes = [...(spec.workspaceTypes ?? [])];
316
- 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()];
317
344
  if (declaresBirth) {
318
345
  const haveInvariant = new Set(workspaceInvariants.map((w) => w.id));
319
- for (const lifecycle of [reclaimChildRequiresRetired, retireChildNotReclaimed, retireChildRequiresBirth]) {
346
+ for (const lifecycle of [
347
+ reclaimChildRequiresRetired,
348
+ reclaimChildAsAdminRequiresRetired,
349
+ retireChildNotReclaimed,
350
+ retireChildAsAdminNotReclaimed,
351
+ retireChildRequiresBirth,
352
+ retireChildAsAdminRequiresBirth,
353
+ ]) {
320
354
  if (!haveInvariant.has(lifecycle.id)) workspaceInvariants.push(lifecycle);
321
355
  }
322
356
  }
@@ -333,10 +367,21 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
333
367
  if (declaresBirth) {
334
368
  const haveQuery = new Set(queries.map((q) => q.id));
335
369
  for (const q of BIRTH_FRAMEWORK_QUERIES) {
336
- if (!haveQuery.has(q.id)) queries.push(q);
370
+ if (!haveQuery.has(q.id)) {
371
+ queries.push({ ...q, businessBirthInternal: true } as QueryDecl);
372
+ }
337
373
  }
338
374
  // The inheritance read (`.reads("authProvider")` on birthHome) routes like any declared query.
339
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
+ }
340
385
  }
341
386
  const relationTypes = [...(spec.relationTypes ?? [])];
342
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 {
@@ -86,6 +86,32 @@ export const retireChild = directive("retireChild")
86
86
  ];
87
87
  });
88
88
 
89
+ /**
90
+ * The operator half of the same retirement action. This is deliberately a separate, framework-internal
91
+ * directive because the relation gate is structural: creator recovery and platform administration have
92
+ * different authority proofs even though the client presents one `retire` action and both write the same
93
+ * lifecycle fact. A broken creator key must never make a malformed child impossible to converge.
94
+ */
95
+ export const retireChildAsAdmin = directive("retireChildAsAdmin")
96
+ .ensures(NomosChildLifecycle)
97
+ .payload(z.object({
98
+ child: z.string().min(1),
99
+ principal: z.string().min(1),
100
+ retiredBy: z.string().min(1),
101
+ retiredAt: z.number().int().nonnegative(),
102
+ }))
103
+ .plan((p) => {
104
+ const rec = instance(NomosChildLifecycle, childLifecycleId(p.child));
105
+ return [
106
+ set(rec, "child", p.child),
107
+ set(rec, "principal", p.principal),
108
+ set(rec, "status", "retired"),
109
+ set(rec, "retiredBy", p.retiredBy),
110
+ set(rec, "retiredAt", p.retiredAt),
111
+ ];
112
+ })
113
+ .requires("admin");
114
+
89
115
  export const reclaimChild = directive("reclaimChild")
90
116
  .ensures(NomosChildLifecycle)
91
117
  .payload(z.object({
@@ -102,6 +128,24 @@ export const reclaimChild = directive("reclaimChild")
102
128
  ];
103
129
  });
104
130
 
131
+ /** Admin-gated recovery half of {@link reclaimChild}; hidden behind the client's one reclaim action. */
132
+ export const reclaimChildAsAdmin = directive("reclaimChildAsAdmin")
133
+ .ensures(NomosChildLifecycle)
134
+ .payload(z.object({
135
+ child: z.string().min(1),
136
+ reclaimedBy: z.string().min(1),
137
+ reclaimedAt: z.number().int().nonnegative(),
138
+ }))
139
+ .plan((p) => {
140
+ const rec = instance(NomosChildLifecycle, childLifecycleId(p.child));
141
+ return [
142
+ set(rec, "status", "reclaimed"),
143
+ set(rec, "reclaimedBy", p.reclaimedBy),
144
+ set(rec, "reclaimedAt", p.reclaimedAt),
145
+ ];
146
+ })
147
+ .requires("admin");
148
+
105
149
  /** Reclaim is two-phase; a child must first be explicitly withdrawn. */
106
150
  export const reclaimChildRequiresRetired = workspaceInvariant("ReclaimChildRequiresRetired")
107
151
  .on("reclaimChild")
@@ -115,6 +159,18 @@ export const reclaimChildRequiresRetired = workspaceInvariant("ReclaimChildRequi
115
159
  : { reject: "reclaim-child-requires-retired" },
116
160
  );
117
161
 
162
+ /** Operator recovery has the same two-phase law, without depending on the broken creator credential. */
163
+ export const reclaimChildAsAdminRequiresRetired = workspaceInvariant("ReclaimChildAsAdminRequiresRetired")
164
+ .on("reclaimChildAsAdmin")
165
+ .reads((ctx) => [
166
+ refAs("record", "NomosChildLifecycle", "pre:" + childLifecycleId(String(ctx.intent.child ?? ""))),
167
+ ])
168
+ .assert(({ record }) =>
169
+ (record as { status?: string } | undefined)?.status === "retired"
170
+ ? { accept: true }
171
+ : { reject: "reclaim-child-requires-retired" },
172
+ );
173
+
118
174
  /** Reclaimed is permanent: retirement cannot move a child back out of the terminal state. */
119
175
  export const retireChildNotReclaimed = workspaceInvariant("RetireChildNotReclaimed")
120
176
  .on("retireChild")
@@ -127,6 +183,18 @@ export const retireChildNotReclaimed = workspaceInvariant("RetireChildNotReclaim
127
183
  : { accept: true },
128
184
  );
129
185
 
186
+ /** Reclaimed is terminal for operator recovery too. */
187
+ export const retireChildAsAdminNotReclaimed = workspaceInvariant("RetireChildAsAdminNotReclaimed")
188
+ .on("retireChildAsAdmin")
189
+ .reads((ctx) => [
190
+ refAs("record", "NomosChildLifecycle", "pre:" + childLifecycleId(String(ctx.intent.child ?? ""))),
191
+ ])
192
+ .assert(({ record }) =>
193
+ (record as { status?: string } | undefined)?.status === "reclaimed"
194
+ ? { reject: "reclaimed-child-is-terminal" }
195
+ : { accept: true },
196
+ );
197
+
130
198
  /** A lifecycle cannot mint authority for arbitrary custody: retirement must name its exact birth witness. */
131
199
  export const retireChildRequiresBirth = workspaceInvariant("RetireChildRequiresBirth")
132
200
  .on("retireChild")
@@ -143,6 +211,20 @@ export const retireChildRequiresBirth = workspaceInvariant("RetireChildRequiresB
143
211
  : { reject: "retire-child-requires-exact-birth-record" };
144
212
  });
145
213
 
214
+ /** Admin recovery may act for the recorded creator, but still cannot invent or retarget custody. */
215
+ export const retireChildAsAdminRequiresBirth = workspaceInvariant("RetireChildAsAdminRequiresBirth")
216
+ .on("retireChildAsAdmin")
217
+ .reads((ctx) => [
218
+ refAs("birth", "NomosBirthRecord", "pre:" + birthRecordId(String(ctx.intent.child ?? ""))),
219
+ ])
220
+ .assert(({ birth }, ctx) => {
221
+ const record = birth as { child?: string; principal?: string } | undefined;
222
+ const intent = ctx?.intent as { child?: string; principal?: string } | undefined;
223
+ return record?.child === intent?.child && record?.principal === intent?.principal
224
+ ? { accept: true }
225
+ : { reject: "retire-child-requires-exact-birth-record" };
226
+ });
227
+
146
228
  function sameLifecyclePrincipal(recorded: string | undefined, author: string | undefined): boolean {
147
229
  const normalize = (s: string | undefined): string => s?.startsWith("user:") ? s.slice(5) : (s ?? "");
148
230
  return normalize(recorded) !== "" && normalize(recorded) === normalize(author);
package/src/index.ts CHANGED
@@ -477,10 +477,15 @@ export {
477
477
  seedBirthPolicy,
478
478
  grantBirthAllowance,
479
479
  retireChild,
480
+ retireChildAsAdmin,
480
481
  reclaimChild,
482
+ reclaimChildAsAdmin,
481
483
  reclaimChildRequiresRetired,
484
+ reclaimChildAsAdminRequiresRetired,
482
485
  retireChildRequiresBirth,
486
+ retireChildAsAdminRequiresBirth,
483
487
  retireChildNotReclaimed,
488
+ retireChildAsAdminNotReclaimed,
484
489
  } from "./framework/birth_meter.js";
485
490
  // THE PLATFORM FOUNDING-ADMIN BOOTSTRAP (the platform analog of seedHomeCreator) — a tenant-importable
486
491
  // factory that seats a tenant-named governance relation for a write-once, author-bound founder at genesis.