@githolon/dsl 0.18.0 → 0.20.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.20.0
2
+ - Authority-aware: `NomosBridge.offerDirective(domain, directiveId, payload, {actor, domainHash})` — generated
3
+ clients route every offer through THE ONE encoder (`encodeIntentOfferBytes` in @githolon/client); no
4
+ per-client envelope encoding. New typed `MissingOfferAuthority` — a `.requires(...)` directive authored
5
+ with no actor throws BEFORE submit (an actorless gated offer cannot be generated). Lockstep jump to 0.20.0.
6
+
1
7
  ## 0.17.0
2
8
  - Lockstep bump with the birth-primitive release (see `@githolon/dsl` 0.17.0): birth is one law primitive
3
9
  (`birthChild`), the platform-creator tier collapsed onto `#creator`, `grantCreation` takes a user or key
@@ -77,3 +77,22 @@ class LawHashUnavailable extends NomosBridgeError {
77
77
  LawHashUnavailable({required this.workspace, String? message})
78
78
  : super(message ?? 'could not resolve the current law hash for "$workspace" — the cloud reported no installed law for the home');
79
79
  }
80
+
81
+ /// AUTHORITY IS UNFORGEABLE-BY-CONSTRUCTION: a GATED directive (one carrying a `.requires(relation)` write
82
+ /// gate in its law) authored with NO `actor` cannot produce an offer — the generated client throws THIS
83
+ /// BEFORE any submit, so an actorless gated offer is impossible to GENERATE (never merely refused at the
84
+ /// kernel gate). REMEDY: construct the generated client with an `actor` (the verified principal — under a
85
+ /// home scope this is the subject; the bridge owns signing). Secrets NEVER appear here — only the identity.
86
+ class MissingOfferAuthority extends NomosBridgeError {
87
+ /// The domain of the gated directive.
88
+ final String domain;
89
+
90
+ /// The gated directive id that could not be offered without an authority.
91
+ final String directiveId;
92
+
93
+ /// The relation the law requires the author to hold (e.g. `admin`).
94
+ final String requires;
95
+ MissingOfferAuthority({required this.domain, required this.directiveId, required this.requires, String? message})
96
+ : super(message ??
97
+ '$domain/$directiveId requires the "$requires" relation — construct the client with an actor (the verified principal) to author it; a gated offer cannot be made without an authority');
98
+ }
@@ -176,6 +176,28 @@ class NomosBridge {
176
176
  return r['head'].toString();
177
177
  }
178
178
 
179
+ /// THE ONE ENCODER offer: hand the holon a PLANNED directive (domain/directiveId/payload) + the verified
180
+ /// [actor]; the runner's `offerDirective` op encodes the v2 envelope via the SINGLE codec (index.mjs
181
+ /// `encodeIntentOfferBytes`, with `security.actor` included iff [actor] is set). The generated Dart client
182
+ /// calls THIS — it never hand-builds offer bytes, so there is exactly ONE copy of the wire format. Returns
183
+ /// the new local head; durable on [sync]. ([offer] with opaque bytes stays for back-compat carriers.)
184
+ Future<String> offerDirective({
185
+ required String domain,
186
+ required String directiveId,
187
+ required Object? payload,
188
+ String? actor,
189
+ String? domainHash,
190
+ }) async {
191
+ final r = _asMap(await _call('offerDirective', {
192
+ 'domain': domain,
193
+ 'directiveId': directiveId,
194
+ 'payload': payload,
195
+ if (actor != null) 'actor': actor,
196
+ if (domainHash != null) 'domainHash': domainHash,
197
+ }));
198
+ return r['head'].toString();
199
+ }
200
+
179
201
  /// Mint an ed25519 DEVICE KEY in-kernel → `{secret, public}` (base64). The SECRET stays on this device
180
202
  /// (persist it in secure storage); pass it to [connect] as `authorSecret` (or per-offer `signWith`) so the
181
203
  /// holon SIGNS each intent. The PUBLIC half is what gets enrolled so the gate re-verifies the signature on
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.18.0
14
+ version: 0.20.0
15
15
 
16
16
  environment:
17
17
  sdk: ^3.5.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.18.0",
3
+ "version": "0.20.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
@@ -50,6 +50,30 @@ export function __drainBirths(): BirthRecipe[] {
50
50
  * kernel executes the birth ONLY for a directive its resolved law names there (the era rule: pre-feature
51
51
  * chains carry no key, so they replay byte-green and the kernel does nothing). A plan may birth at most one
52
52
  * child per intent (one offer, one defined consequence — the unix model).
53
+ *
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
+ *
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
+ * ```
53
77
  */
54
78
  export function birth(recipe: BirthRecipe): void {
55
79
  footprint.push(recipe);
@@ -123,15 +123,39 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
123
123
  const payloadClass = `${cap(d.id)}Payload`;
124
124
  const stamps = autoStampFields((d as unknown as { payloadSchema: z.ZodTypeAny }).payloadSchema);
125
125
  const method = safeMethod(d.id);
126
+ // THE AUTHORITY GATE (codegen-time): a directive carrying `.requires(relation)` is GATED — its method
127
+ // throws MissingOfferAuthority when `actor` is null/empty, BEFORE any submit, so an actorless gated
128
+ // offer is impossible to GENERATE (the war: the gate is structural, not just kernel-side). Secrets never
129
+ // appear — only the actor identity (the bridge owns signing).
130
+ const gate = (d as { requiresRelationGate?: { relation: string } }).requiresRelationGate;
131
+ const gateGuard = gate !== undefined
132
+ ? ` if (actor == null || actor!.trim().isEmpty) throw MissingOfferAuthority(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, requires: ${dartStr(gate.relation)});`
133
+ : "";
134
+ const gateDoc = gate !== undefined
135
+ ? ` /// GATED: needs an \`actor\` (the \`${gate.relation}\` relation) — throws MissingOfferAuthority otherwise.`
136
+ : "";
126
137
  if (stamps.length > 0) {
127
138
  methods.push(
128
139
  ` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
129
140
  ` /// Omitted ${stamps.map((s) => `\`${s}\``).join(" / ")} auto-stamp(s) ISO now() at offer.`,
141
+ ...(gateDoc ? [gateDoc] : []),
130
142
  ` Future<String> ${method}(${payloadClass} payload) {`,
143
+ ...(gateGuard ? [gateGuard] : []),
131
144
  ` final data = payload.toPayloadJson();`,
132
145
  ` final now = DateTime.now().toUtc().toIso8601String();`,
133
146
  ...stamps.map((s) => ` data.putIfAbsent(${dartStr(s)}, () => now);`),
134
- ` return bridge.offer(intentBytes: _encodeIntentOffer(${dartStr(domain)}, ${dartStr(d.id)}, data, domainHash));`,
147
+ ` return bridge.offerDirective(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: data, actor: actor, domainHash: domainHash);`,
148
+ ` }`,
149
+ ``,
150
+ );
151
+ } else if (gateGuard) {
152
+ // A gated directive with no `…At` sugar still needs a block body for the throw guard.
153
+ methods.push(
154
+ ` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
155
+ gateDoc,
156
+ ` Future<String> ${method}(${payloadClass} payload) {`,
157
+ gateGuard,
158
+ ` return bridge.offerDirective(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: payload.toPayloadJson(), actor: actor, domainHash: domainHash);`,
135
159
  ` }`,
136
160
  ``,
137
161
  );
@@ -139,7 +163,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
139
163
  methods.push(
140
164
  ` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
141
165
  ` Future<String> ${method}(${payloadClass} payload) =>`,
142
- ` bridge.offer(intentBytes: _encodeIntentOffer(${dartStr(domain)}, ${dartStr(d.id)}, payload.toPayloadJson(), domainHash));`,
166
+ ` bridge.offerDirective(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: payload.toPayloadJson(), actor: actor, domainHash: domainHash);`,
143
167
  ``,
144
168
  );
145
169
  }
@@ -216,14 +240,15 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
216
240
  `// final app = ${className}(bridge);`,
217
241
  `// await app.${dirs[0] ? safeMethod(dirs[0].id) : "someDirective"}(/* typed payload */);`,
218
242
  ``,
219
- `import 'dart:convert';`,
220
- ``,
243
+ // `dart:convert` is needed ONLY for the interface block's `jsonDecode` — the offer lane no longer encodes
244
+ // any envelope (it routes through `bridge.offerDirective` → THE ONE ENCODER). Omit it when no interface.
245
+ ...(opts.interface !== undefined ? [`import 'dart:convert';`, ``] : []),
221
246
  `import '${supportImport}';`,
222
247
  `import '${mod.name}.dart';`,
223
248
  `// Re-export the sibling types (read models + payloads + the bridge runtime) so ONE import of this`,
224
249
  `// client file gives an app the whole typed surface.`,
225
250
  `export '${mod.name}.dart';`,
226
- `export '${supportImport}' show NomosBridge, BridgeTransport, NomosBridgeError, kWebviewBridgeVersion;`,
251
+ `export '${supportImport}' show NomosBridge, BridgeTransport, NomosBridgeError, MissingOfferAuthority, kWebviewBridgeVersion;`,
227
252
  ``,
228
253
  `/// sha256 of the deployed \`.package.usda\` — the installed law this client offers under.`,
229
254
  `const String ${hashConst} = ${dartStr(opts.domainHash)};`,
@@ -238,22 +263,22 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
238
263
  ``,
239
264
  ]
240
265
  : []),
241
- `String _encodeIntentOffer(String domain, String directiveId, Object? payload, String domainHash) =>`,
242
- ` base64Encode(utf8.encode(jsonEncode({`,
243
- ` 'v': 2,`,
244
- ` 'intent': {`,
245
- ` 'domain': domain,`,
246
- ` 'directiveId': directiveId,`,
247
- ` 'payload': payload,`,
248
- ` 'domainHash': domainHash,`,
249
- ` },`,
250
- ` })));`,
251
- ``,
252
266
  `/// Typed async client for the \`${domain}\` domain, bound to a connected [NomosBridge].`,
267
+ `///`,
268
+ `/// Pass \`actor\` (the verified principal uid) to author AS that user — the bridge's \`offerDirective\` op`,
269
+ `/// carries it through THE ONE ENCODER (\`encodeIntentOfferBytes\` in @githolon/client) as \`security.actor\`,`,
270
+ `/// so the kernel seals it as the captured principal and the ReBAC \`.requires(...)\` gate judges the real`,
271
+ `/// author. A GATED directive (one whose law declares \`.requires(relation)\`) authored with no actor throws`,
272
+ `/// [MissingOfferAuthority] BEFORE any submit — an actorless gated offer can't be constructed. The bridge`,
273
+ `/// owns SIGNING (a device secret on \`connect\`); only the actor IDENTITY is passed here, never a secret.`,
253
274
  `class ${className} {`,
254
275
  ` final NomosBridge bridge;`,
255
276
  ` final String domainHash;`,
256
- ` ${className}(this.bridge, {this.domainHash = ${hashConst}});`,
277
+ ``,
278
+ ` /// The verified principal uid every offer is authored as (sealed as \`security.actor\` by the one encoder).`,
279
+ ` /// Null ⇒ open (un-gated) authoring; a gated directive then throws [MissingOfferAuthority].`,
280
+ ` final String? actor;`,
281
+ ` ${className}(this.bridge, {this.domainHash = ${hashConst}, this.actor});`,
257
282
  ``,
258
283
  ...(opts.interface !== undefined
259
284
  ? [
@@ -42,6 +42,10 @@ import type { DomainModule } from "./codegen_dart.js";
42
42
  import type { QueryDecl } from "./query.js";
43
43
  import { finishCount } from "./count.js";
44
44
  import { mintedCreateField } from "./workspace_routing.js";
45
+ import { deterministicPorts } from "./ctx.js";
46
+ import { __resetBirths, __drainBirths } from "./births.js";
47
+ import { __resetAuthoring, __drainAuthoring } from "./authoring.js";
48
+ import { __resetReads, __drainReads } from "./read.js";
45
49
  import {
46
50
  autoStampFields,
47
51
  tsClientFactoryName,
@@ -179,6 +183,353 @@ function findConcurrencyLeg(
179
183
  return undefined;
180
184
  }
181
185
 
186
+ // ── authority-conformance discovery (the ONE-ENCODER + gate proof — actorless gated offer is impossible) ──
187
+
188
+ /** A discovered AUTHORITY-CONFORMANCE leg: a GATED directive (one carrying `.requires(relation)`) the proof
189
+ * can drive locally to prove (a) authored WITH an actor, the produced offer envelope carries `security.actor`
190
+ * (the one encoder threaded the actor through), and (b) authored WITHOUT an actor, the generated client
191
+ * THROWS MissingOfferAuthority before any submit (an actorless gated offer is impossible to GENERATE). */
192
+ export interface AuthorityConformanceLeg {
193
+ /** The gated directive's domain (the generated factory + its `domain` field). */
194
+ readonly domain: string;
195
+ /** The gated directive id (the generated client method). */
196
+ readonly directiveId: string;
197
+ /** The relation the law requires the author to hold (e.g. `admin`) — the MissingOfferAuthority `requires`. */
198
+ readonly relation: string;
199
+ /** The synthesized payload the proof drives the gated directive with (auto-stamped `…At` omitted). */
200
+ readonly payload: Record<string, unknown>;
201
+ /** Required `…At` fields the runtime stamps at dispatch (mirrors the create leg). */
202
+ readonly autoStamped: readonly string[];
203
+ /** When the gated directive is a `.creates` whose own id is kernel-minted, the field to mint at runtime. */
204
+ readonly mintField?: string;
205
+ }
206
+
207
+ /** Discover the AUTHORITY-CONFORMANCE leg: the first GATED directive (`.requires(relation)` ⇒
208
+ * `requiresRelationGate`) whose payload the synthesizer can sample. A birth / attestation-authorized
209
+ * directive is skipped (their authority is a carried cert / signed quote, not an `actor` block — a different
210
+ * lane). `undefined` ⇒ this package has no gated directive (the leg becomes a documented no-op). */
211
+ export function discoverAuthorityConformanceLeg(mod: DomainModule): AuthorityConformanceLeg | undefined {
212
+ const domain = mod.domain ?? mod.name;
213
+ const gated = (mod.directives as Directive<unknown>[]).find(
214
+ (d) =>
215
+ d?.requiresRelationGate !== undefined &&
216
+ typeof d.payloadSchema === "object" &&
217
+ d.birthsChild !== true &&
218
+ d.attestationAuthorizedGate === undefined,
219
+ );
220
+ if (gated === undefined) return undefined;
221
+ let synth: SynthesizedPayload;
222
+ try {
223
+ synth = synthesizePayload(gated.payloadSchema as z.ZodTypeAny, `${domain}/${gated.id}`);
224
+ } catch {
225
+ return undefined; // an exotic payload the synthesizer can't sample → no conformance leg (still gated at the kernel)
226
+ }
227
+ const agg = (mod.aggregates as AggregateHandle[]).find((a) => a.id === gated.aggregateId);
228
+ const mintField = agg !== undefined ? mintedCreateField(gated, agg) : undefined;
229
+ const payload: Record<string, unknown> = { ...synth.value };
230
+ if (mintField !== undefined) delete payload[mintField];
231
+ return {
232
+ domain,
233
+ directiveId: gated.id,
234
+ relation: gated.requiresRelationGate!.relation,
235
+ payload,
236
+ autoStamped: synth.autoStamped,
237
+ ...(mintField !== undefined ? { mintField } : {}),
238
+ };
239
+ }
240
+
241
+ /**
242
+ * THE AUTHORITY-CONFORMANCE LEG (the one-encoder + gate proof). On the real LOCAL holon, with fetch trapped:
243
+ * (a) bind the generated client WITH an actor + drive the gated directive. SPY on `holon.offerDirective` to
244
+ * capture the (domain, directiveId, payload, opts) the client passes, RE-ENCODE that through the v2
245
+ * envelope shape (the SAME `{ v:2, intent, security:{actor} }` the one encoder emits) and DECODE it back
246
+ * to assert `security.actor` IS the bound actor — the produced offer envelope carries the principal.
247
+ * (b) bind a SECOND client WITHOUT an actor + call the gated directive — assert it THROWS
248
+ * MissingOfferAuthority LOCALLY (before any offerDirective/submit fires; the spy stays untouched).
249
+ * Emits nothing when the package has no gated directive (a documented no-op note).
250
+ */
251
+ function authorityConformanceLegLines(
252
+ leg: AuthorityConformanceLeg | undefined,
253
+ factory: string,
254
+ n: () => number,
255
+ ): string[] {
256
+ if (leg === undefined) {
257
+ return [
258
+ `// ${n()}. authority-conformance leg: this law has no gated (\`.requires(...)\`) directive, so there is`,
259
+ `// nothing to prove about actorless-gated-offer refusal (an un-gated domain authors openly).`,
260
+ `console.log("– authority-conformance leg SKIPPED — no .requires(...) directive (add one to prove the one-encoder threads security.actor + an actorless gated offer can't be generated)");`,
261
+ ``,
262
+ ];
263
+ }
264
+ const payloadEntries = leg.mintField !== undefined
265
+ ? `{ ...${JSON.stringify(leg.payload)}, ${JSON.stringify(leg.mintField)}: await holon.mint("conformance") }`
266
+ : JSON.stringify(leg.payload);
267
+ return [
268
+ `// ${n()}. AUTHORITY CONFORMANCE (THE ONE ENCODER + the gate): \`${leg.domain}/${leg.directiveId}\` is GATED`,
269
+ `// (\`.requires("${leg.relation}")\`). (a) WITH an actor the produced offer envelope must carry`,
270
+ `// security.actor; (b) WITHOUT one the generated client THROWS MissingOfferAuthority before any submit.`,
271
+ `{`,
272
+ ` const realFetch2 = globalThis.fetch;`,
273
+ ` globalThis.fetch = (() => { throw new Error("OFFLINE PROOF VIOLATED: authority-conformance touched the network"); }) as typeof fetch;`,
274
+ ` try {`,
275
+ ` // (a) SPY on offerDirective: capture what the generated client passes, re-encode the v2 envelope it`,
276
+ ` // would produce (the one-encoder shape), decode it back, and assert security.actor IS the actor.`,
277
+ ` const ACTOR = "user:proof-conformance";`,
278
+ ` let captured: { domain: string; directiveId: string; payload: unknown; opts?: { domainHash?: string; actor?: string } } | undefined;`,
279
+ ` const realOffer: typeof holon.offerDirective = holon.offerDirective.bind(holon);`,
280
+ ` holon.offerDirective = (domain, directiveId, payload, opts) => {`,
281
+ ` captured = { domain, directiveId, payload, opts };`,
282
+ ` return realOffer(domain, directiveId, payload, opts);`,
283
+ ` };`,
284
+ ` try {`,
285
+ ` await ${factory}(holon, { actor: ACTOR }).${leg.directiveId}(${payloadEntries});`,
286
+ ` } finally {`,
287
+ ` holon.offerDirective = realOffer;`,
288
+ ` }`,
289
+ ` if (!captured) fail("authority-conformance: the gated directive did not reach holon.offerDirective");`,
290
+ ` const actorOnOffer = captured!.opts?.actor;`,
291
+ ` if (actorOnOffer !== ACTOR) fail(\`authority-conformance: the offer did not carry the actor (got \${JSON.stringify(actorOnOffer)})\`);`,
292
+ ` // Decode the bytes THE ONE ENCODER actually emits from these args (NOT a hand-rolled copy) — so this`,
293
+ ` // leg catches encoder DRIFT (a dropped/renamed/whitespace-normalized security.actor), not merely that`,
294
+ ` // the actor reached offerDirective. The v2 wire format lives in exactly one place: encodeIntentOfferBytes.`,
295
+ ` const decoded = JSON.parse(new TextDecoder().decode(encodeIntentOfferBytes({ domain: captured!.domain, directiveId: captured!.directiveId, payload: captured!.payload, domainHash: captured!.opts?.domainHash, actor: actorOnOffer })));`,
296
+ ` if (decoded?.security?.actor !== ACTOR) fail(\`authority-conformance: THE ONE ENCODER dropped security.actor (\${JSON.stringify(decoded?.security)})\`);`,
297
+ ` ok(\`authority conformance (a) — \${${JSON.stringify(`${leg.domain}/${leg.directiveId}`)}} authored WITH an actor carries security.actor through THE ONE ENCODER\`);`,
298
+ ``,
299
+ ` // (b) the SAME directive with NO actor: the generated client refuses to BUILD the offer — it throws`,
300
+ ` // MissingOfferAuthority locally, BEFORE any submit (an actorless gated offer is impossible to make).`,
301
+ ` let threw: Error | undefined;`,
302
+ ` try {`,
303
+ ` await ${factory}(holon).${leg.directiveId}(${payloadEntries});`,
304
+ ` } catch (e) { threw = e as Error; }`,
305
+ ` if (threw === undefined) fail("authority-conformance: the gated directive with NO actor did NOT throw — an actorless gated offer must be impossible to generate");`,
306
+ ` if (threw.name !== "MissingOfferAuthority") fail(\`authority-conformance: expected MissingOfferAuthority, got \${threw.name}: \${threw.message}\`);`,
307
+ ` ok(\`authority conformance (b) — \${${JSON.stringify(`${leg.domain}/${leg.directiveId}`)}} with NO actor throws MissingOfferAuthority before any submit (gated offer impossible to generate)\`);`,
308
+ ` } finally {`,
309
+ ` globalThis.fetch = realFetch2;`,
310
+ ` }`,
311
+ `}`,
312
+ ``,
313
+ ];
314
+ }
315
+
316
+ // ── child-birth discovery (the G3 `.births()` lane — prove a parent birth end-to-end into the child) ──────
317
+
318
+ /** ONE step of a discovered birth recipe's genesis chain (the subset codegen reads). */
319
+ interface DiscoveredGenesisStep {
320
+ readonly domain: string;
321
+ readonly directiveId: string;
322
+ }
323
+
324
+ /** A discovered CHILD-BIRTH leg: a parent `.births()` directive proven end-to-end into the child it births
325
+ * (install the framework + child law in-chain → run the child genesis seed → mount the born child →
326
+ * verify it). The runtime obtains the actual born WORKSPACE NAME from the offer's `born` result (the recipe
327
+ * workspace is plan-derived — often `…-<sha256(owner)>` — so it is not statically known), and injects the
328
+ * REAL `frameworkHash`/`lawHash` (the local wasm's controller hash + the package domainHash) onto the
329
+ * synthesized payload (the synthesizer only produces `sample-*` placeholders, which the child install
330
+ * would refuse). */
331
+ export interface ChildBirthLeg {
332
+ /** The domain the `.births()` directive lives in. */
333
+ readonly parentDomain: string;
334
+ /** The `.births()` directive id the parent drives to birth the child. */
335
+ readonly birthDirectiveId: string;
336
+ /** The synthesized parent payload (auto-stamped `…At` omitted; `frameworkHash`/`lawHash` placeholders
337
+ * REMOVED — the runtime injects the real hashes onto these fields by name). */
338
+ readonly birthPayload: Record<string, unknown>;
339
+ /** Required `…At` fields the runtime stamps with ISO now() at dispatch (mirrors the create leg). */
340
+ readonly birthAutoStamped: readonly string[];
341
+ /** The payload field that carries the bootstrap framework-controller hash (runtime: the local wasm's own
342
+ * controller hash) — discovered from the recipe's `bootstrap/installDomain` step's payload, by matching
343
+ * the field whose synthesized value the recipe routed there. `undefined` ⇒ the recipe hard-codes it. */
344
+ readonly frameworkHashField?: string;
345
+ /** The payload field that carries the child law hash (runtime: the deployed package domainHash). */
346
+ readonly lawHashField?: string;
347
+ /** The CHILD domain key the recipe's seed step targets (e.g. `estate`) — the child-only law installed
348
+ * by this birth. This is what makes a child-only domain proof-addressable (gaps 1+3). */
349
+ readonly childDomain: string;
350
+ /** The child genesis SEED directive the recipe runs (e.g. `seedEstateWorkspace`). */
351
+ readonly childSeedDirectiveId: string;
352
+ /** The aggregate the child seed `.ensures`/`.creates` (e.g. `EstateOwner`) — read back to prove the seed ran. */
353
+ readonly childSeedAggregateId?: string;
354
+ /** The instance id the child seed writes, when it is a fixed singleton (`<Agg>:self` / a constant id) —
355
+ * lets the proof read the seeded record by id with no query. `undefined` ⇒ fall back to the child query. */
356
+ readonly childSeedInstanceId?: string;
357
+ /** A child declared query that reads the seed back (returns the seed aggregate). `undefined` ⇒ by-id only. */
358
+ readonly childQuery?: { readonly id: string; readonly key: readonly string[] };
359
+ }
360
+
361
+ /** A stub `nomos` global (mint + sha256) so a `.births()` plan that mints / hashes a child name runs to
362
+ * completion at codegen. The hash is a deterministic STUB (the codegen never needs the real digest — the
363
+ * recipe's workspace is read at RUNTIME from the offer's `born`); mint mirrors `harvestPrincipalFlows`. */
364
+ function withBirthProbeGlobals<T>(run: () => T): T {
365
+ const g = globalThis as unknown as { nomos?: { mint?: (t: string) => string; sha256?: (s: string) => string; read?: unknown } };
366
+ const had = "nomos" in g;
367
+ const prior = g.nomos;
368
+ g.nomos = {
369
+ ...(prior ?? {}),
370
+ mint: (typeTag: string) => `${typeTag}_0192f00d-0000-7000-8000-000000000000`,
371
+ // A deterministic 64-hex STUB digest (length-stable so a `.slice(0, n)` name template still works).
372
+ sha256: (s: string) => {
373
+ let h = 2166136261 >>> 0;
374
+ for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619) >>> 0; }
375
+ return h.toString(16).padStart(8, "0").repeat(8);
376
+ },
377
+ };
378
+ try {
379
+ return run();
380
+ } finally {
381
+ if (had && prior !== undefined) g.nomos = prior; else delete g.nomos;
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Discover the CHILD-BIRTH leg a `.births()` directive declares: run its plan with a synthesized payload +
387
+ * the birth-probe globals, drain the recorded birth recipe (`birth(recipe)` → `__drainBirths`), and read the
388
+ * recipe's genesis chain to learn the CHILD domain + seed directive + the framework/law install fields.
389
+ *
390
+ * This is the SAME compile-time plan-execution technique `harvestPrincipalFlows` (the endorse discovery) and
391
+ * `directiveRoutes`/`codegen_dart` already use — the plan is a pure function of (payload, ports). Best-effort:
392
+ * a plan that can't be probed (or doesn't record a 3-step `installDomain×2 + seed` leaf recipe) returns
393
+ * `undefined` and the directive proves only `compile-valid`. Never throws (drains the sinks on any path).
394
+ */
395
+ export function discoverChildBirthLeg(
396
+ birthDir: Directive<unknown>,
397
+ parentDomain: string,
398
+ modules: readonly DomainModule[],
399
+ ): ChildBirthLeg | undefined {
400
+ if (birthDir.birthsChild !== true) return undefined;
401
+ let recipe: { workspace?: unknown; genesisChain?: unknown } | undefined;
402
+ let synth: SynthesizedPayload;
403
+ try {
404
+ synth = synthesizePayload(birthDir.payloadSchema as z.ZodTypeAny, `${parentDomain}/${birthDir.id}`);
405
+ } catch {
406
+ return undefined;
407
+ }
408
+ // The plan needs concrete payload VALUES for every field it reads to build the recipe (frameworkHash /
409
+ // lawHash / owner …). The synthesizer omits `…At` (auto-stamped) + optional fields; fill the auto-stamped
410
+ // ones with a probe ISO string so the plan runs (the values are irrelevant to discovery — only the recipe
411
+ // SHAPE is read).
412
+ const probePayload: Record<string, unknown> = { ...synth.value };
413
+ for (const f of synth.autoStamped) probePayload[f] = "2026-01-01T00:00:00Z";
414
+ try {
415
+ withBirthProbeGlobals(() => {
416
+ __resetAuthoring();
417
+ __resetReads();
418
+ __resetBirths();
419
+ try {
420
+ birthDir.plan(probePayload as never, deterministicPorts({ physical: 1, replica: 1, principal: "principal:probe" }));
421
+ } finally {
422
+ try { __drainAuthoring(); __drainReads(); } catch { /* sinks may be drained */ }
423
+ }
424
+ const drained = __drainBirths();
425
+ recipe = drained[0] as typeof recipe;
426
+ });
427
+ } catch {
428
+ return undefined;
429
+ }
430
+ if (recipe === undefined || !Array.isArray(recipe.genesisChain)) return undefined;
431
+ const chain = recipe.genesisChain as DiscoveredGenesisStep[];
432
+ // The child seed = the FIRST genesis step that is neither bootstrap nor nomos installDomain (the leaf's
433
+ // own seed). A platform-shaped recipe (seedGenesis → grantAdmin → birthCloud tail) is out of scope here —
434
+ // we prove the LEAF birth shape (install×2 + one seed), the home/estate composable case.
435
+ const seedStep = chain.find(
436
+ (s) => s !== null && typeof s === "object" && s.directiveId !== "installDomain"
437
+ && s.domain !== "bootstrap" && s.domain !== "nomos",
438
+ );
439
+ if (seedStep === undefined) return undefined;
440
+ const childDomain = seedStep.domain;
441
+ const childSeedDirectiveId = seedStep.directiveId;
442
+
443
+ // Resolve the child seed's aggregate + singleton id + read query from the CHILD domain module (so the
444
+ // child read-model leg reads the right thing). The child law is in THIS package (the one-package pattern
445
+ // — the parent folds it so the kernel can route its bytes by hash).
446
+ const childMod = modules.find((m) => (m.domain ?? m.name) === childDomain);
447
+ let childSeedAggregateId: string | undefined;
448
+ let childSeedInstanceId: string | undefined;
449
+ let childQuery: ChildBirthLeg["childQuery"];
450
+ if (childMod !== undefined) {
451
+ const seedDir = (childMod.directives as Directive<unknown>[]).find((d) => d?.id === childSeedDirectiveId);
452
+ if (seedDir !== undefined) {
453
+ childSeedAggregateId = seedDir.aggregateId;
454
+ childSeedInstanceId = discoverSingletonInstanceId(seedDir);
455
+ const q = ((childMod.queries ?? []) as QueryDecl[]).find((qq) => qq.returns === seedDir.aggregateId && qq.key.length > 0);
456
+ if (q !== undefined) childQuery = { id: q.id, key: q.key };
457
+ }
458
+ }
459
+
460
+ // The framework/law install FIELDS: which parent-payload field the recipe routed into each install step's
461
+ // `domainHash`. The recipe step's payload value === the parent payload value for that field — match by value.
462
+ const installSteps = chain.filter((s) => s !== null && typeof s === "object" && s.directiveId === "installDomain") as Array<DiscoveredGenesisStep & { payload?: { domainHash?: unknown } }>;
463
+ const fieldForValue = (v: unknown): string | undefined =>
464
+ Object.entries(synth.value).find(([, sv]) => sv === v && typeof v === "string" && v.length > 0)?.[0];
465
+ const bootstrapStep = installSteps.find((s) => s.domain === "bootstrap");
466
+ const nomosStep = installSteps.find((s) => s.domain === "nomos");
467
+ const frameworkHashField = bootstrapStep ? fieldForValue(bootstrapStep.payload?.domainHash) : undefined;
468
+ const lawHashField = nomosStep ? fieldForValue(nomosStep.payload?.domainHash) : undefined;
469
+
470
+ // The parent payload the leg dispatches: the synthesized values MINUS the framework/law-hash placeholders
471
+ // (the runtime injects the REAL hashes onto those fields by name — a `sample-*` hash would fail the child
472
+ // install) MINUS the auto-stamped fields (stamped at dispatch).
473
+ const birthPayload: Record<string, unknown> = { ...synth.value };
474
+ if (frameworkHashField !== undefined) delete birthPayload[frameworkHashField];
475
+ if (lawHashField !== undefined) delete birthPayload[lawHashField];
476
+
477
+ return {
478
+ parentDomain,
479
+ birthDirectiveId: birthDir.id,
480
+ birthPayload,
481
+ birthAutoStamped: synth.autoStamped,
482
+ ...(frameworkHashField !== undefined ? { frameworkHashField } : {}),
483
+ ...(lawHashField !== undefined ? { lawHashField } : {}),
484
+ childDomain,
485
+ childSeedDirectiveId,
486
+ ...(childSeedAggregateId !== undefined ? { childSeedAggregateId } : {}),
487
+ ...(childSeedInstanceId !== undefined ? { childSeedInstanceId } : {}),
488
+ ...(childQuery !== undefined ? { childQuery } : {}),
489
+ };
490
+ }
491
+
492
+ /** Discover the SINGLETON instance id a seed directive writes (a fixed `<Agg>:self`-shaped id), by running
493
+ * its plan with a probe payload + birth-probe globals and reading the first written op's instance id when
494
+ * it is a constant (the same id across two distinct probe payloads). `undefined` ⇒ not a fixed singleton. */
495
+ function discoverSingletonInstanceId(seedDir: Directive<unknown>): string | undefined {
496
+ const idsFor = (seed: number): string | undefined => {
497
+ let synth: SynthesizedPayload;
498
+ try {
499
+ synth = synthesizePayload(seedDir.payloadSchema as z.ZodTypeAny, `seed/${seedDir.id}`);
500
+ } catch {
501
+ return undefined;
502
+ }
503
+ const payload: Record<string, unknown> = { ...synth.value };
504
+ for (const f of synth.autoStamped) payload[f] = `2026-0${seed}-01T00:00:00Z`;
505
+ // Make string fields distinct per-seed so a payload-derived id would DIFFER across runs (a true
506
+ // singleton id is constant regardless).
507
+ for (const [k, v] of Object.entries(payload)) if (typeof v === "string" && !synth.autoStamped.includes(k)) payload[k] = `${v}-${seed}`;
508
+ let id: string | undefined;
509
+ withBirthProbeGlobals(() => {
510
+ __resetAuthoring();
511
+ __resetReads();
512
+ __resetBirths();
513
+ try {
514
+ const returned = seedDir.plan(payload as never, deterministicPorts({ physical: seed, replica: 1 }));
515
+ const ops = [...__drainAuthoring(), ...(Array.isArray(returned) ? returned : [])];
516
+ const first = ops.find((o) => (o as { aggregateId?: string }).aggregateId !== undefined) as { aggregateId?: string } | undefined;
517
+ id = first?.aggregateId;
518
+ } catch {
519
+ id = undefined;
520
+ } finally {
521
+ try { __drainReads(); __drainBirths(); } catch { /* drained */ }
522
+ }
523
+ });
524
+ return id;
525
+ };
526
+ const a = idsFor(1);
527
+ const b = idsFor(2);
528
+ // A fixed singleton: a non-empty id that does NOT vary with the payload AND isn't a per-instance type tag
529
+ // (a TYPE id equals the aggregate type — that's the legacy one-per-workspace shape, also fine to read by id).
530
+ return a !== undefined && a === b ? a : undefined;
531
+ }
532
+
182
533
  // ── the generator ─────────────────────────────────────────────────────────────────
183
534
 
184
535
  export interface TsProofOptions {
@@ -188,28 +539,51 @@ export interface TsProofOptions {
188
539
  readonly domainHash: string;
189
540
  }
190
541
 
542
+ /** THE PROOF TAXONOMY (co2 feedback): which legs a domain carries.
543
+ * · `compile-valid` — the law compiled (always — every compiled domain is at least this).
544
+ * · `standalone-provable` — a `.creates` write proven offline (dispatch → declared query/count locally).
545
+ * · `child-birth-provable` — a `.births()` directive proven END-TO-END into the child it births.
546
+ * · `live-cloud-provable` — the full cloud loop (the `.mts` `--live` lane) completes (the primary domain). */
547
+ export type ProofKind = "compile-valid" | "standalone-provable" | "child-birth-provable" | "live-cloud-provable";
548
+
191
549
  /** The machine-readable OFFLINE legs for ONE domain (the `@nomos-proof-legs`
192
550
  * marker shape; the `proof-legs.json` index is a map of these by domain key). */
193
551
  export interface ProofLegs {
194
552
  readonly domain: string;
195
- readonly directiveId: string;
196
- readonly aggregateId: string;
197
- readonly payload: Record<string, unknown>;
198
- readonly autoStamped: readonly string[];
553
+ /** THE TAXONOMY (co2 feedback) — the legs this domain has, in proof order. EVERY domain has at least
554
+ * `compile-valid`. OPTIONAL + ADDITIVE: an older-compiler legs file without it is treated as
555
+ * `["standalone-provable"]` (it only emitted standalone-provable domains). */
556
+ readonly legs?: readonly ProofKind[];
557
+ /** The standalone-create directive id — present iff this domain is `standalone-provable`. */
558
+ readonly directiveId?: string;
559
+ readonly aggregateId?: string;
560
+ readonly payload?: Record<string, unknown>;
561
+ readonly autoStamped?: readonly string[];
199
562
  readonly mintField?: string;
200
563
  readonly query?: { readonly id: string; readonly params: Record<string, unknown> };
201
564
  readonly count?: { readonly id: string; readonly group: string };
565
+ /** THE CHILD-BIRTH LEG — present iff this domain has a `.births()` directive (it is `child-birth-provable`).
566
+ * When this domain is itself the BORN child's law (no standalone create), the index also registers the
567
+ * child domain key pointing at the SAME leg, so `proof --domain <child>` resolves through the parent birth. */
568
+ readonly childBirth?: ChildBirthLeg;
202
569
  }
203
570
 
204
- /** Synthesize the offline legs for one domain module, or `null` when it has no
205
- * creating write (nothing to prove). The SINGLE source of the leg-selection
206
- * logic — `generateTsProof` (the .mts marker) and `proofLegsIndex` (the
207
- * per-domain index) both call this, so the offline lane proves any domain the
571
+ /** Synthesize the standalone-create offline legs for one domain module, or `null` when it has no
572
+ * creating write (nothing to prove standalone — it may still be child-birth-provable / compile-valid).
573
+ * The SINGLE source of the create-leg selection — `generateTsProof` (the .mts marker) and
574
+ * `proofLegsIndex` (the per-domain index) both call this, so the offline lane proves any domain the
208
575
  * same way the generated proof proves its primary one. */
576
+ /** A `.creates` directive the proof can DRIVE from a synthesized sample offline: not a `.births()` (a birth
577
+ * is the child-birth lane), not ATTESTATION-AUTHORIZED (needs a signed §3 quote no synthesizer can produce —
578
+ * e.g. the home law's `recordShare`). Those are NOT standalone-provable offline; the domain proves via its
579
+ * child-birth leg / compile-valid instead. */
580
+ function isStandaloneCreate(d: Directive<unknown> | undefined): boolean {
581
+ return d?.marker === "creates" && typeof d.payloadSchema === "object"
582
+ && d.birthsChild !== true && d.attestationAuthorizedGate === undefined;
583
+ }
584
+
209
585
  export function proofLegsForModule(mod: DomainModule): ProofLegs | null {
210
- const createDir = (mod.directives as Directive<unknown>[]).find(
211
- (d) => d?.marker === "creates" && typeof d.payloadSchema === "object",
212
- );
586
+ const createDir = (mod.directives as Directive<unknown>[]).find((d) => isStandaloneCreate(d));
213
587
  if (createDir === undefined) return null;
214
588
  const domain = mod.domain ?? mod.name;
215
589
  const agg = (mod.aggregates as AggregateHandle[]).find((a) => a.id === createDir.aggregateId);
@@ -226,6 +600,7 @@ export function proofLegsForModule(mod: DomainModule): ProofLegs | null {
226
600
  .find((c) => c.of === agg.id && (c as { where?: unknown }).where == null && (c.by == null || baseValue[c.by] !== undefined));
227
601
  return {
228
602
  domain,
603
+ legs: ["compile-valid", "standalone-provable"],
229
604
  directiveId: createDir.id,
230
605
  aggregateId: agg.id,
231
606
  payload: baseValue,
@@ -236,49 +611,309 @@ export function proofLegsForModule(mod: DomainModule): ProofLegs | null {
236
611
  };
237
612
  }
238
613
 
239
- /** The per-domain legs index for `build/<name>.proof-legs.json` — every domain
240
- * with a creating write, plus the default (the first, which the .mts proves).
241
- * `githolon proof [--domain <key>]` runs any of these offline. */
614
+ /** The first `.births()` directive in a module (with a synthesizable payload), or undefined. */
615
+ function birthDirectiveOf(mod: DomainModule): Directive<unknown> | undefined {
616
+ return (mod.directives as Directive<unknown>[]).find((d) => d?.birthsChild === true && typeof d.payloadSchema === "object");
617
+ }
618
+
619
+ /** Build the legs for ONE domain module — every leg it carries (compile-valid always; standalone-provable
620
+ * when it has a `.creates`; child-birth-provable when it has a `.births()`). NEVER null: every compiled
621
+ * domain is at least `compile-valid` (gaps 1+3 — every domain is proof-addressable). */
622
+ export function legsForDomain(mod: DomainModule, modules: readonly DomainModule[]): ProofLegs {
623
+ const domain = mod.domain ?? mod.name;
624
+ // The standalone-create leg (when the domain has a non-birth `.creates`). FAIL SOFT: an exotic payload
625
+ // shape the synthesizer can't sample just drops the standalone leg (the domain stays compile-valid /
626
+ // child-birth-provable), never loses the index.
627
+ let standalone: ProofLegs | null = null;
628
+ try {
629
+ standalone = proofLegsForModule(mod);
630
+ } catch {
631
+ standalone = null;
632
+ }
633
+ // The child-birth leg (when the domain has a `.births()` directive) — discovered by running its plan.
634
+ const birthDir = birthDirectiveOf(mod);
635
+ const childBirth = birthDir !== undefined ? discoverChildBirthLeg(birthDir, domain, modules) : undefined;
636
+
637
+ const legs: ProofKind[] = ["compile-valid"];
638
+ if (standalone !== null) legs.push("standalone-provable");
639
+ if (childBirth !== undefined) legs.push("child-birth-provable");
640
+
641
+ return {
642
+ domain,
643
+ legs,
644
+ ...(standalone !== null
645
+ ? {
646
+ directiveId: standalone.directiveId,
647
+ aggregateId: standalone.aggregateId,
648
+ payload: standalone.payload,
649
+ autoStamped: standalone.autoStamped,
650
+ ...(standalone.mintField !== undefined ? { mintField: standalone.mintField } : {}),
651
+ ...(standalone.query !== undefined ? { query: standalone.query } : {}),
652
+ ...(standalone.count !== undefined ? { count: standalone.count } : {}),
653
+ }
654
+ : {}),
655
+ ...(childBirth !== undefined ? { childBirth } : {}),
656
+ };
657
+ }
658
+
659
+ /** The per-domain legs index for `build/<name>.proof-legs.json` — EVERY compiled domain is addressable
660
+ * (gaps 1+3, co2 feedback), with its taxonomy (compile-valid | standalone-provable | child-birth-provable).
661
+ * A CHILD-ONLY law (no standalone create, installed only via a parent birth) is registered under its OWN
662
+ * domain key pointing at the parent birth that proves it. `githolon proof [--domain <key>]` resolves for
663
+ * every domain. The default is the first STANDALONE-provable domain (the live `.mts` lane needs one), else
664
+ * the first child-birth-provable, else the first compiled domain. */
242
665
  export function proofLegsIndex(modules: readonly DomainModule[]): { default: string; domains: Record<string, ProofLegs> } | null {
243
666
  const domains: Record<string, ProofLegs> = {};
244
- let first: string | undefined;
667
+ const order: string[] = [];
245
668
  for (const mod of modules) {
246
- // FAIL SOFT per-module: a sibling domain whose payload can't be synthesized
247
- // (an exotic zod shape synthesizePayload throws on) must not lose the index
248
- // for the domains that DO synthesize — skip it, keep the rest.
249
- let legs: ProofLegs | null;
250
- try {
251
- legs = proofLegsForModule(mod);
252
- } catch {
253
- continue;
254
- }
255
- if (legs === null) continue;
669
+ const legs = legsForDomain(mod, modules);
670
+ if (domains[legs.domain] === undefined) order.push(legs.domain);
256
671
  domains[legs.domain] = legs;
257
- if (first === undefined) first = legs.domain;
258
672
  }
259
- return first === undefined ? null : { default: first, domains };
673
+ // CHILD-DOMAIN ADDRESSABILITY: a `.births()` directive names a CHILD domain (e.g. `estate`) that may have
674
+ // no standalone leg of its own. Register that child domain key pointing at the PARENT's child-birth leg
675
+ // (so `proof --domain estate` proves it THROUGH the birth that installs it — the only way it comes to be).
676
+ for (const legs of [...Object.values(domains)]) {
677
+ const cb = legs.childBirth;
678
+ if (cb === undefined) continue;
679
+ const existing = domains[cb.childDomain];
680
+ if (existing === undefined) {
681
+ // A pure child-only domain whose ONLY proof is the parent birth: register it as child-birth-provable.
682
+ domains[cb.childDomain] = { domain: cb.childDomain, legs: ["compile-valid", "child-birth-provable"], childBirth: cb };
683
+ if (!order.includes(cb.childDomain)) order.push(cb.childDomain);
684
+ } else if (existing.childBirth === undefined) {
685
+ // The child domain ALSO compiled standalone (e.g. it has its own create) — add the child-birth leg to it.
686
+ const mergedLegs = [...new Set([...(existing.legs ?? ["compile-valid"]), "child-birth-provable" as const])];
687
+ domains[cb.childDomain] = { ...existing, legs: mergedLegs, childBirth: cb };
688
+ }
689
+ }
690
+ if (order.length === 0) return null;
691
+ const standaloneFirst = order.find((d) => (domains[d]!.legs ?? []).includes("standalone-provable"));
692
+ const childBirthFirst = order.find((d) => (domains[d]!.legs ?? []).includes("child-birth-provable"));
693
+ const def = standaloneFirst ?? childBirthFirst ?? order[0]!;
694
+ return { default: def, domains };
260
695
  }
261
696
 
262
- export function generateTsProof(modules: readonly DomainModule[], opts: TsProofOptions): string {
263
- // The proof drives the FIRST domain that has a creating write — without one
264
- // there is nothing to author, so there is nothing to prove.
265
- const mod = modules.find((m) =>
266
- (m.directives as Directive<unknown>[]).some((d) => d?.marker === "creates" && typeof d.payloadSchema === "object"),
697
+ // ── shared `.mts` fragments (the v2 contract — G1: no host secret; deploy is OPEN + kernel-gated) ─────────
698
+
699
+ /**
700
+ * THE V2 PREAMBLE (G1, gap 4): create a throwaway workspace (NO secret comes back — ownership is a ledger
701
+ * fact), then deploy YOUR compiled law via the OPEN lane (`POST /domains`, no Bearer; write-authority is
702
+ * kernel-judged at the offer gate). Identity rides `x-nomos-principal` (transitional bare-uid lane), the
703
+ * same header `cli/src/cloud.ts deploy()` + the 0.18 host-strip lane present. Declares `fail`/`ok`/`deploy`/
704
+ * `r`/`d`/`runError`/`PRINCIPAL` and opens the `try {` (closed by {@link v2CatchFinally}).
705
+ */
706
+ function v2IdentityAndCleanupPreamble(opts: TsProofOptions, hashConst: string, n: () => number): string[] {
707
+ return [
708
+ `// IDENTITY NOTE: this proof uses the bare x-nomos-principal lane so it is SELF-CONTAINED (a throwaway`,
709
+ `// workspace, no stored credentials). Real apps use \`githolon login --agent\` (a VERIFIED identity)`,
710
+ `// + \`githolon ws create/deploy\`. (G1: a create returns NO workspaceSecret — ownership is a ledger`,
711
+ `// fact; deploy is OPEN + kernel-gated, authenticated by the principal, not a host secret.)`,
712
+ `const fail: (m: string) => never = (m) => { throw new Error(m); };`,
713
+ `const ok = (m: string) => console.log("✓ " + m);`,
714
+ `const PRINCIPAL = process.env.NOMOS_PRINCIPAL || "githolon-proof";`,
715
+ ``,
716
+ `const deploy = JSON.parse(readFileSync(new URL(${JSON.stringify(`./${opts.packageName}.deploy.json`)}, import.meta.url), "utf8"));`,
717
+ ``,
718
+ `// CLEANUP CONTRACT: the throwaway workspace is RETIRED on every exit (green or jammed) — a proof never`,
719
+ `// leaves orphans. PROOF_KEEP=1 (githolon proof --live --keep) keeps it and prints the persist command.`,
720
+ `let runError: unknown = null;`,
721
+ `let born_ws = false;`,
722
+ `try {`,
723
+ ``,
724
+ `// ${n()}. a throwaway workspace — born by the create (G1: NO secret is returned; ownership is a ledger fact)`,
725
+ `let r = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}\`, { method: "POST", headers: { "x-nomos-principal": PRINCIPAL } });`,
726
+ `let d = await r.json();`,
727
+ `if (!d.ok) fail(\`create workspace: \${JSON.stringify(d)}\`);`,
728
+ `born_ws = true;`,
729
+ `ok(\`workspace \${WS} created (no host secret — ownership is a ledger fact; PROOF_KEEP=1 keeps it)\`);`,
730
+ ``,
731
+ `// ${n()}. deploy YOUR compiled law via the OPEN lane (POST /domains, no Bearer — kernel-judged authority,`,
732
+ `// authenticated as PRINCIPAL via x-nomos-principal; a refusal answers as a TYPED kernel verdict 422)`,
733
+ `r = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/domains\`, { method: "POST", headers: { "content-type": "application/json", "x-nomos-principal": PRINCIPAL }, body: JSON.stringify(deploy) });`,
734
+ `d = await r.json();`,
735
+ `if (!d.ok || d.installation?.[0]?.data?.["status.phase"] !== "Active") fail(\`deploy: \${JSON.stringify(d)}\`);`,
736
+ `if (d.domainHash !== ${hashConst}) fail(\`deploy hash \${d.domainHash} != the generated client's \${${hashConst}}\`);`,
737
+ `ok(\`law deployed Active — \${${hashConst}.slice(0, 12)}… (the content hash baked into your typed client)\`);`,
738
+ ];
739
+ }
740
+
741
+ /**
742
+ * THE V2 CATCH/FINALLY (G1, gap 4): retire the throwaway workspace on every exit via the OPEN lane (no
743
+ * Bearer — there is no host secret to present; a retire is a SIGNED governance offer in the real lane, but
744
+ * the open self-retire suffices to clean up a throwaway). PROOF_KEEP=1 keeps it.
745
+ */
746
+ function v2CatchFinally(): string[] {
747
+ return [
748
+ `} catch (e) {`,
749
+ ` runError = e;`,
750
+ ` console.error("✗ " + ((e as Error)?.message ?? e));`,
751
+ `} finally {`,
752
+ ` // THE CLEANUP CONTRACT: never an orphaned cloud workspace. Retired on every exit path (open lane — no`,
753
+ ` // host secret exists post-G1); PROOF_KEEP=1 keeps it and prints the persist command.`,
754
+ ` if (born_ws && process.env.PROOF_KEEP === "1") {`,
755
+ ` console.log(\`workspace \${WS} KEPT — retire it when done: githolon ws retire \${WS}\`);`,
756
+ ` } else if (born_ws) {`,
757
+ ` const rr = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/retire\`, { method: "POST", headers: { "x-nomos-principal": PRINCIPAL } }).then((x) => x.json()).catch(() => ({}));`,
758
+ ` console.log(\` retired \${WS}: \${(rr as { ok?: boolean }).ok === true}\`);`,
759
+ ` }`,
760
+ ` process.exit(runError ? 1 : 0);`,
761
+ `}`,
762
+ ];
763
+ }
764
+
765
+ /**
766
+ * THE LIVE CHILD-BIRTH LEG (G3, gap 2): drive the parent `.births()` directive against the cloud workspace,
767
+ * then read the BORN CHILD workspace it spawned (the kernel's offer-effect installs the child law in-chain +
768
+ * folds the child genesis through the child's OWN gate). Verifies the child came up BORN (its read model has
769
+ * the seeded record) — the cloud's offer response surfaces `born: [{workspace, head}]`. The frameworkHash +
770
+ * lawHash are injected from the deployed law (the child install resolves bytes by hash from the parent's
771
+ * custody; a `sample-*` hash would fail). The dispatch rides the gateway offer lane (authenticated principal).
772
+ */
773
+ function liveChildBirthLegLines(cb: ChildBirthLeg, opts: TsProofOptions, n: () => number): string[] {
774
+ const hashConst = tsHashConstName(opts.packageName);
775
+ // The birth payload literal: the synthesized values + the REAL hashes onto the discovered fields + the
776
+ // auto-stamped `…At` fields stamped now.
777
+ const lines: string[] = [];
778
+ lines.push(
779
+ `// ${n()}. CHILD BIRTH (G3 — birth is the defined consequence of an offer): drive \`${cb.parentDomain}/${cb.birthDirectiveId}\``,
780
+ `// against the cloud; the kernel's offer-effect installs the child law in-chain + folds the child's`,
781
+ `// genesis (estate/${cb.childSeedDirectiveId}) through the CHILD's own gate, then BORNS the child workspace.`,
782
+ `{`,
783
+ ` const birthPayload: Record<string, unknown> = { ...${JSON.stringify(cb.birthPayload)} };`,
784
+ ...(cb.frameworkHashField !== undefined
785
+ ? [
786
+ ` // The framework CONTROLLER hash (the bootstrap install-target the child runs as intent 0) — read it`,
787
+ ` // from the workspace's own status (the kernel's controllerHash); the child install resolves the bytes`,
788
+ ` // by hash from the parent's custody, so this MUST be the real framework hash (a guess would fail).`,
789
+ ` const st = await (await fetch(\`\${CLOUD}/v2/workspaces/\${WS}\`)).json().catch(() => ({}));`,
790
+ ` const frameworkHash = st.controller?.domainHash ?? st.controllerHash ?? "";`,
791
+ ` if (!frameworkHash) fail("could not resolve the framework controller hash from the workspace status — needed for the child's in-chain installDomain");`,
792
+ ` birthPayload[${JSON.stringify(cb.frameworkHashField)}] = frameworkHash;`,
793
+ ]
794
+ : []),
795
+ ` birthPayload[${JSON.stringify(cb.lawHashField ?? "lawHash")}] = ${hashConst};`,
796
+ ...cb.birthAutoStamped.map((f) => ` birthPayload[${JSON.stringify(f)}] = new Date().toISOString();`),
797
+ ` const br = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/${cb.birthDirectiveId}\`, { method: "POST", headers: { "content-type": "application/json", "x-nomos-principal": PRINCIPAL }, body: JSON.stringify(birthPayload) });`,
798
+ ` const bd = await br.json();`,
799
+ ` if (!bd.ok) fail(\`child birth offer ${cb.parentDomain}/${cb.birthDirectiveId} refused: \${JSON.stringify(bd)}\`);`,
800
+ ` const born: Array<{ workspace?: string }> = bd.born ?? [];`,
801
+ ` const childWs = born[0]?.workspace;`,
802
+ ` if (!childWs) fail(\`birth admitted but the offer-effect spawned no child (born=\${JSON.stringify(born)})\`);`,
803
+ ` ok(\`child birth — \${childWs} born by ${cb.parentDomain}/${cb.birthDirectiveId} (the offer-effect installed the child law + folded its genesis)\`);`,
804
+ ` // The BORN child runs its OWN law: read the seeded ${cb.childSeedAggregateId ?? "record"} back from the child workspace.`,
805
+ ...(cb.childSeedInstanceId !== undefined
806
+ ? [
807
+ ` const cr = await fetch(\`\${CLOUD}/v2/workspaces/\${childWs}/aggregates/\${encodeURIComponent(${JSON.stringify(cb.childSeedInstanceId)})}\`).then((x) => x.json()).catch(() => ({}));`,
808
+ ` if (!(cr.rows?.length >= 1)) fail(\`the born child did not seed ${cb.childSeedAggregateId ?? "its record"} (${cb.childSeedInstanceId}): \${JSON.stringify(cr)}\`);`,
809
+ ` ok(\`the born child's read model has the genesis seed — ${cb.childSeedDirectiveId} ran inside \${childWs}\`);`,
810
+ ]
811
+ : cb.childQuery !== undefined
812
+ ? [
813
+ ` const cr = await fetch(\`\${CLOUD}/v2/workspaces/\${childWs}/${cb.childQuery.id}?${cb.childQuery.key.map((k) => `${encodeURIComponent(k)}=\${encodeURIComponent(String(birthPayload[${JSON.stringify(k)}] ?? ""))}`).join("&")}\`).then((x) => x.json()).catch(() => ({}));`,
814
+ ` if (!(cr.ok && cr.rows?.length >= 1)) fail(\`the born child's declared query ${cb.childQuery.id} returned nothing: \${JSON.stringify(cr)}\`);`,
815
+ ` ok(\`the born child's declared query ${cb.childQuery.id} returns the genesis seed — ${cb.childSeedDirectiveId} ran inside \${childWs}\`);`,
816
+ ]
817
+ : [
818
+ ` ok(\`the child \${childWs} is born (no declared read on its seed aggregate — declare a query to read it back)\`);`,
819
+ ]),
820
+ `}`,
821
+ ``,
822
+ );
823
+ return lines;
824
+ }
825
+
826
+ /**
827
+ * THE BIRTH-ONLY PROOF (gap 2): a package whose primary write is a `.births()` directive (no standalone
828
+ * create — the home/estate composable shape). Deploy the parent law, drive the birth, verify the born child.
829
+ * (The offline runner proves the same legs LOCALLY on the vendored wasm — that is the default `githolon
830
+ * proof` lane; this `.mts` is the live `--live` lane.)
831
+ */
832
+ function generateBirthOnlyProof(
833
+ mod: DomainModule,
834
+ modules: readonly DomainModule[],
835
+ opts: TsProofOptions,
836
+ primaryLegs: ProofLegs,
837
+ ): string {
838
+ void modules;
839
+ const domain = mod.domain ?? mod.name;
840
+ const factory = tsClientFactoryName(domain);
841
+ const hashConst = tsHashConstName(opts.packageName);
842
+ const cb = primaryLegs.childBirth;
843
+ if (cb === undefined) {
844
+ throw new Error(
845
+ `domain '${domain}' has a .births() directive but its child-birth leg could not be discovered (the plan did not record a leaf genesis recipe: bootstrap/installDomain → nomos/installDomain → <child>/<seed>) — prove it by hand in test/e2e.mts`,
846
+ );
847
+ }
848
+ const slug =
849
+ opts.packageName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24) || "domain";
850
+ const legsMarker = JSON.stringify(primaryLegs);
851
+ let step = 0;
852
+ const n = () => ++step;
853
+ const out: string[] = [];
854
+ out.push(
855
+ `// AUTO-GENERATED by nomos-compile — the RUNNABLE PROOF for package "${opts.packageName}",`,
856
+ `// synthesized from the domain's OWN law. This package's primary write is a BIRTH (a .births()`,
857
+ `// directive), so the proof drives the birth end-to-end into the BORN child. Reshape the law,`,
858
+ `// recompile, and this proof reshapes itself — no test rewriting. Run it:`,
859
+ `//`,
860
+ `// npx githolon proof (offline legs on a local holon)`,
861
+ `// npx githolon proof --live (or: npx tsx build/${opts.packageName}.proof.mts — the full cloud loop)`,
862
+ `//`,
863
+ `// Regenerated on every compile; do not edit.`,
864
+ `// @nomos-proof-legs ${legsMarker}`,
865
+ `import { readFileSync } from "node:fs";`,
866
+ `import { connect, encodeIntentOfferBytes } from "@githolon/client";`,
867
+ `import { ${factory}, ${hashConst} } from "./${opts.packageName}.client.ts";`,
868
+ `void connect; void ${factory};`,
869
+ ``,
870
+ `const CLOUD = (process.env.NOMOS_CLOUD || "https://nomos.captainapp.co.uk").replace(/\\/+$/, "");`,
871
+ `const WS = process.env.NOMOS_WS || ${JSON.stringify(slug + "-proof-")} + Math.random().toString(36).slice(2, 8);`,
872
+ ``,
873
+ ...v2IdentityAndCleanupPreamble(opts, hashConst, n),
874
+ ``,
875
+ ...liveChildBirthLegLines(cb, opts, n),
876
+ `console.log(\`\\nALL GREEN — generated proof for "${opts.packageName}": deploy → child birth (\${WS} → the born child) → verified\`);`,
877
+ ``,
878
+ ...v2CatchFinally(),
879
+ ``,
267
880
  );
881
+ return out.join("\n");
882
+ }
883
+
884
+ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofOptions): string {
885
+ // PRIMARY-DOMAIN SELECTION: the live `.mts` drives a STANDALONE create when the package has one (the full
886
+ // deploy → write → query → sync → cloud loop). A BIRTH-ONLY package (every `.creates` is also a `.births()`
887
+ // — the home/estate composable shape) has no standalone create to drive, so the primary becomes the first
888
+ // `.births()` domain and the live `.mts` drives the birth as its primary write. Either way, the marker =
889
+ // the primary domain's FULL legs (so the offline runner + the index never describe different directives).
890
+ const hasStandaloneCreate = (m: DomainModule): boolean =>
891
+ (m.directives as Directive<unknown>[]).some((d) => isStandaloneCreate(d));
892
+ const hasBirth = (m: DomainModule): boolean => birthDirectiveOf(m) !== undefined;
893
+ const mod = modules.find(hasStandaloneCreate) ?? modules.find(hasBirth);
268
894
  if (mod === undefined) {
269
895
  throw new Error(
270
- `no .creates directive in any domain — a generated proof needs one creating write; add a directive(...).creates(...) (test/e2e.mts remains the hand-authored lane)`,
896
+ `no .creates directive in any domain — a generated proof needs one creating write (or a .births() birth directive); add a directive(...).creates(...) (test/e2e.mts remains the hand-authored lane)`,
271
897
  );
272
898
  }
273
899
  const domain = mod.domain ?? mod.name;
274
900
  const factory = tsClientFactoryName(domain);
275
901
  const hashConst = tsHashConstName(opts.packageName);
902
+ // THE MARKER (since 0.5.0) + the per-domain index are ONE synthesis (`legsForDomain`) — the proof body, its
903
+ // marker, and the index can never describe different directives. The primary domain's legs carry whichever
904
+ // of standalone / child-birth it has.
905
+ const primaryLegs = legsForDomain(mod, modules);
906
+ const childBirth = primaryLegs.childBirth;
276
907
 
277
- // SAME predicate as the mod selection above AND proofLegsForModule — so the
278
- // proof body, its marker, and the index can never describe different directives.
908
+ // The standalone-create lane (when the primary has one). A birth-only primary skips this — its primary
909
+ // write is the child-birth leg below.
279
910
  const createDir = (mod.directives as Directive<unknown>[]).find(
280
- (d) => d?.marker === "creates" && typeof d.payloadSchema === "object",
281
- )!;
911
+ (d) => d?.marker === "creates" && typeof d.payloadSchema === "object" && d.birthsChild !== true,
912
+ );
913
+ if (createDir === undefined) {
914
+ // BIRTH-ONLY PRIMARY: emit a birth-shaped proof (no standalone create/query/count/concurrency legs).
915
+ return generateBirthOnlyProof(mod, modules, opts, primaryLegs);
916
+ }
282
917
  const agg = (mod.aggregates as AggregateHandle[]).find((a) => a.id === createDir.aggregateId);
283
918
  if (agg === undefined) {
284
919
  throw new Error(
@@ -318,6 +953,11 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
318
953
 
319
954
  const concurrency = findConcurrencyLeg(mod.directives as Directive<unknown>[], agg);
320
955
 
956
+ // THE AUTHORITY-CONFORMANCE LEG (the one-encoder + gate proof): the first gated (`.requires(...)`) directive
957
+ // in the primary domain, proven locally — actor threads through the one encoder; an actorless gated offer
958
+ // throws MissingOfferAuthority. `undefined` ⇒ a documented no-op (the domain authors openly).
959
+ const conformance = discoverAuthorityConformanceLeg(mod);
960
+
321
961
  // Workspace-name slug: any package name → a safe, recognizable ws prefix.
322
962
  const slug =
323
963
  opts.packageName
@@ -341,9 +981,9 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
341
981
  const listAccessor = `list${agg.id}s`;
342
982
 
343
983
  // THE MACHINE-READABLE LEGS (parsed by `githolon`'s offline proof runner —
344
- // cli/src/proof_offline.ts). The ONE leg-synthesis (`proofLegsForModule`) feeds
984
+ // cli/src/proof_offline.ts). The ONE leg-synthesis (`legsForDomain`) feeds
345
985
  // both this marker and the per-domain `proof-legs.json` index, so they never drift.
346
- const legsMarker = JSON.stringify(proofLegsForModule(mod));
986
+ const legsMarker = JSON.stringify(primaryLegs);
347
987
 
348
988
  let step = 0;
349
989
  const n = () => ++step;
@@ -360,41 +1000,13 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
360
1000
  `// every call — start at docs/01-mental-model.md). Regenerated on every compile; do not edit.`,
361
1001
  `// @nomos-proof-legs ${legsMarker}`,
362
1002
  `import { readFileSync } from "node:fs";`,
363
- `import { connect } from "@githolon/client";`,
1003
+ `import { connect, encodeIntentOfferBytes } from "@githolon/client";`,
364
1004
  `import { ${factory}, ${hashConst} } from "./${opts.packageName}.client.ts";`,
365
1005
  ``,
366
1006
  `const CLOUD = (process.env.NOMOS_CLOUD || "https://nomos.captainapp.co.uk").replace(/\\/+$/, "");`,
367
1007
  `const WS = process.env.NOMOS_WS || ${JSON.stringify(slug + "-proof-")} + Math.random().toString(36).slice(2, 8);`,
368
1008
  ``,
369
- `// IDENTITY NOTE: this proof uses the bare x-nomos-principal lane so it is`,
370
- `// SELF-CONTAINED (a throwaway workspace, no stored credentials). Real apps use`,
371
- `// \`githolon login --agent\` (a VERIFIED identity) + \`githolon ws create/deploy\`.`,
372
- `const fail: (m: string) => never = (m) => { throw new Error(m); };`,
373
- `const ok = (m: string) => console.log("✓ " + m);`,
374
- ``,
375
- `const deploy = JSON.parse(readFileSync(new URL(${JSON.stringify(`./${opts.packageName}.deploy.json`)}, import.meta.url), "utf8"));`,
376
- ``,
377
- `// CLEANUP CONTRACT: the throwaway workspace is RETIRED on every exit (green or`,
378
- `// jammed) — a proof never leaves orphans. PROOF_KEEP=1 (githolon proof --live --keep)`,
379
- `// keeps it and prints the FULL secret once, with the persist command.`,
380
- `let SECRET = "";`,
381
- `let runError: unknown = null;`,
382
- `try {`,
383
- ``,
384
- `// ${n()}. a throwaway workspace (the ONE-TIME secret comes back on create)`,
385
- `let r = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}\`, { method: "POST", headers: { "x-nomos-principal": "githolon-proof" } });`,
386
- `let d = await r.json();`,
387
- `if (!d.ok) fail(\`create workspace: \${JSON.stringify(d)}\`);`,
388
- `SECRET = d.workspaceSecret;`,
389
- `if (!SECRET?.startsWith("nws_v1_")) fail(\`no workspaceSecret returned: \${JSON.stringify(d)}\`);`,
390
- `ok(\`workspace \${WS} created (one-time secret held for cleanup — PROOF_KEEP=1 keeps the workspace and prints it)\`);`,
391
- ``,
392
- `// ${n()}. deploy YOUR compiled law (build/${opts.packageName}.deploy.json) WITH the secret`,
393
- `r = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/domains\`, { method: "POST", headers: { "content-type": "application/json", authorization: \`Bearer \${SECRET}\` }, body: JSON.stringify(deploy) });`,
394
- `d = await r.json();`,
395
- `if (!d.ok || d.installation?.[0]?.data?.["status.phase"] !== "Active") fail(\`deploy: \${JSON.stringify(d)}\`);`,
396
- `if (d.domainHash !== ${hashConst}) fail(\`deploy hash \${d.domainHash} != the generated client's \${${hashConst}}\`);`,
397
- `ok(\`law deployed Active — \${${hashConst}.slice(0, 12)}… (the content hash baked into your typed client)\`);`,
1009
+ ...v2IdentityAndCleanupPreamble(opts, hashConst, n),
398
1010
  ``,
399
1011
  `// ${n()}. connect + bind the GENERATED typed client`,
400
1012
  `const holon = await connect({ cloud: CLOUD, workspace: WS, clientId: "proof-1" });`,
@@ -532,6 +1144,17 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
532
1144
  );
533
1145
  }
534
1146
 
1147
+ // THE AUTHORITY-CONFORMANCE LEG (the one-encoder + gate proof): runs on the LOCAL holon (fetch re-trapped),
1148
+ // proving the generated client threads security.actor through THE ONE ENCODER and that an actorless gated
1149
+ // offer is impossible to GENERATE (throws MissingOfferAuthority before any submit). No-op when un-gated.
1150
+ out.push(...authorityConformanceLegLines(conformance, factory, n));
1151
+
1152
+ // THE CHILD-BIRTH LEG (G3, gap 2): when the primary domain has a `.births()` directive, prove the birth
1153
+ // END-TO-END against the cloud — drive the parent birth offer, then read the BORN child workspace it spawns.
1154
+ if (childBirth !== undefined) {
1155
+ out.push(...liveChildBirthLegLines(childBirth, opts, n));
1156
+ }
1157
+
535
1158
  out.push(
536
1159
  `// ${n()}. CONVERGENCE: the original instance adopts canonical main in place — no reconnect`,
537
1160
  `await holon.pull();`,
@@ -541,23 +1164,9 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
541
1164
  `if (head !== remoteMain) fail(\`not converged in place: local \${head?.slice(0, 10)} vs main \${remoteMain?.slice(0, 10)}\`);`,
542
1165
  `ok("converged in place — local head == canonical main");`,
543
1166
  ``,
544
- `console.log(\`\\nALL GREEN — generated proof for "${opts.packageName}": deploy → offline write → local reads → admission → cloud reads${concurrency ? ` → AddWins merge` : ``} → convergence (\${WS})\`);`,
1167
+ `console.log(\`\\nALL GREEN — generated proof for "${opts.packageName}": deploy → offline write → local reads → admission → cloud reads${concurrency ? ` → AddWins merge` : ``}${childBirth !== undefined ? ` → child birth` : ``} → convergence (\${WS})\`);`,
545
1168
  ``,
546
- `} catch (e) {`,
547
- ` runError = e;`,
548
- ` console.error("✗ " + ((e as Error)?.message ?? e));`,
549
- `} finally {`,
550
- ` // THE CLEANUP CONTRACT: never an orphaned cloud workspace. Retired on every`,
551
- ` // exit path; PROOF_KEEP=1 keeps it and prints the FULL secret exactly once.`,
552
- ` if (SECRET && process.env.PROOF_KEEP === "1") {`,
553
- ` console.log(\`workspace \${WS} KEPT — secret \${SECRET}\`);`,
554
- ` console.log(\` persist it: githolon secret set \${WS} <the secret above> (then: githolon ws retire \${WS} when done)\`);`,
555
- ` } else if (SECRET) {`,
556
- ` const rr = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/retire\`, { method: "POST", headers: { authorization: \`Bearer \${SECRET}\` } }).then((x) => x.json()).catch(() => ({}));`,
557
- ` console.log(\` retired \${WS}: \${(rr as { ok?: boolean }).ok === true}\`);`,
558
- ` }`,
559
- ` process.exit(runError ? 1 : 0);`,
560
- `}`,
1169
+ ...v2CatchFinally(),
561
1170
  ``,
562
1171
  );
563
1172
 
package/src/codegen_ts.ts CHANGED
@@ -344,6 +344,11 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
344
344
  ``,
345
345
  `/** The holon surface this client binds to (structurally satisfied by @githolon/client). */`,
346
346
  `export interface NomosHolon {`,
347
+ ` /** THE ONE ENCODER offer (the single v2-envelope codec lives in @githolon/client \`encodeIntentOfferBytes\`):`,
348
+ ` * hand the holon a PLANNED directive + the verified \`actor\`; the holon encodes + offers. The generated`,
349
+ ` * client calls THIS, never building offer bytes itself — so there is exactly ONE copy of the wire format. */`,
350
+ ` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string }): Promise<string>;`,
351
+ ` /** Opaque-bytes offer — retained for carriers that already hold an encoded envelope (back-compat). */`,
347
352
  ` offer(intentBytes: string | Uint8Array): Promise<string>;`,
348
353
  ` query(queryId: string, params?: Record<string, unknown>): Promise<unknown>;`,
349
354
  ` queryById(aggregateId: string): Promise<unknown>;`,
@@ -364,8 +369,16 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
364
369
  ` discardDeadLetter(id: string): Promise<{ ok: boolean; removed: number }>;`,
365
370
  `}`,
366
371
  ``,
367
- `const encodeIntentOffer = (domain: string, directiveId: string, payload: unknown, domainHash: string, actor?: string): Uint8Array =>`,
368
- ` new TextEncoder().encode(JSON.stringify({ v: 2, intent: { domain, directiveId, payload, domainHash }, ...(actor ? { security: { actor } } : {}) }));`,
372
+ `/** AUTHORITY IS UNFORGEABLE-BY-CONSTRUCTION: a GATED directive (one carrying a \`.requires(relation)\` write`,
373
+ ` * gate) authored with NO \`actor\` cannot produce an offer — it throws THIS before any submit, so an`,
374
+ ` * actorless gated offer is impossible to GENERATE, never merely refused at the kernel gate. Bind the`,
375
+ ` * client with \`{ actor }\` (the verified principal) to author a gated directive. */`,
376
+ `export class MissingOfferAuthority extends Error {`,
377
+ ` constructor(readonly domain: string, readonly directiveId: string, readonly requires: string) {`,
378
+ ` super(\`\${domain}/\${directiveId} requires the "\${requires}" relation — bind the client with { actor } (the verified principal) to author it; a gated offer cannot be made without an authority\`);`,
379
+ ` this.name = "MissingOfferAuthority";`,
380
+ ` }`,
381
+ `}`,
369
382
  ``,
370
383
  );
371
384
 
@@ -632,6 +645,13 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
632
645
  const stamps = autoStampFields(schema);
633
646
  const mint = mintPlan?.get(d.id);
634
647
  const placed = placementDirs.has(d.id) ? ["shard"] : [];
648
+ // THE AUTHORITY GATE (codegen-time): a directive carrying `.requires(relation)` is GATED — its
649
+ // generated method refuses to encode an offer without an `actor` (throws MissingOfferAuthority
650
+ // BEFORE any submit), so an actorless gated offer is impossible to GENERATE, not merely refused.
651
+ const gate = (d as { requiresRelationGate?: { relation: string } }).requiresRelationGate;
652
+ const gateGuard = gate !== undefined
653
+ ? ` if (!actor || !actor.trim()) throw new MissingOfferAuthority(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${JSON.stringify(gate.relation)});\n`
654
+ : "";
635
655
  out.push(
636
656
  `/** \`${domain}/${d.id}\` payload (mirrors the engine's zod schema).${stamps.length ? ` Omitted ${stamps.map((s) => `\`${s}\``).join("/")} auto-stamp(s) ISO now() at offer.` : ""}${mint ? ` Omitted \`${mint.field}\` is KERNEL-MINTED at offer (returned on the result).` : ""}${placed.length ? " Omitted `shard` is picked LEAST-LOADED at offer (the routed holon)." : ""} */`,
637
657
  `export type ${payloadName} = ${tsTypeOfPayload(schema, stamps, mint ? [mint.field] : [], placed)};`,
@@ -649,21 +669,35 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
649
669
  : `{ ...payload, ${mint.field} }`;
650
670
  methods.push(
651
671
  ` /** Author \`${domain}/${d.id}\`: the front-door MINTS \`${mint.field}\` (route-tagged) when omitted`,
652
- ` * — ids stay internal plumbing — and returns it with the new local head. */`,
672
+ ` * — ids stay internal plumbing — and returns it with the new local head.${gate ? ` GATED: needs \`{ actor }\` (\`${gate.relation}\`).` : ""} */`,
653
673
  ` ${d.id}: async (payload: ${payloadName}): Promise<{ head: string; ${mint.field}: string }> => {`,
674
+ ...(gateGuard ? [gateGuard.replace(/\n$/, "")] : []),
654
675
  ` const ${mint.field} = payload.${mint.field} ?? (await (holon as NomosRoutedHolon).mint(${JSON.stringify(mint.mintType)}${mintOpts}));`,
655
- ` const head = await holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, domainHash, actor));`,
676
+ ` const head = await holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, { domainHash, actor });`,
656
677
  ` return { head, ${mint.field} };`,
657
678
  ` },`,
658
679
  );
659
680
  continue;
660
681
  }
661
- methods.push(
662
- ` /** Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head. */`,
663
- stamps.length
664
- ? ` ${d.id}: (payload: ${payloadName}) => holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, stampNow(payload, ${JSON.stringify(stamps)}), domainHash, actor)),`
665
- : ` ${d.id}: (payload: ${payloadName}) => holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, payload, domainHash, actor)),`,
666
- );
682
+ const offerPayload = stamps.length ? `stampNow(payload, ${JSON.stringify(stamps)})` : `payload`;
683
+ const offerCall = `holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${offerPayload}, { domainHash, actor })`;
684
+ if (gateGuard) {
685
+ // GATED directive: a block body that throws MissingOfferAuthority when no actor is bound — an
686
+ // actorless gated offer can't be constructed (the war: the gate is structural, not just kernel-side).
687
+ methods.push(
688
+ ` /** Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
689
+ ` * GATED: needs \`{ actor }\` (the \`${gate!.relation}\` relation) — throws MissingOfferAuthority otherwise. */`,
690
+ ` ${d.id}: (payload: ${payloadName}): Promise<string> => {`,
691
+ gateGuard.replace(/\n$/, ""),
692
+ ` return ${offerCall};`,
693
+ ` },`,
694
+ );
695
+ } else {
696
+ methods.push(
697
+ ` /** Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head. */`,
698
+ ` ${d.id}: (payload: ${payloadName}) => ${offerCall},`,
699
+ );
700
+ }
667
701
  }
668
702
 
669
703
  for (const q of (mod.queries ?? []) as QueryDecl[]) {
@@ -1452,8 +1452,8 @@ async function main(): Promise<void> {
1452
1452
  console.log(` deploy ${rel(deployPath)}`);
1453
1453
  console.log(``);
1454
1454
  console.log(`deploy (Nomos Cloud):`);
1455
- console.log(` githolon ws create <ws> && githolon deploy <ws> # the secret is saved + sent for you`);
1456
- console.log(` # raw lane: curl -X POST -H 'content-type: application/json' -H 'Authorization: Bearer <workspaceSecret>' \\`);
1455
+ console.log(` githolon login --agent && githolon ws create <ws> && githolon deploy <ws> # no secret — deploy is OPEN, kernel-judged at the offer gate`);
1456
+ console.log(` # raw lane: curl -X POST -H 'content-type: application/json' -H 'x-nomos-principal: <your-auth-uid>' \\`);
1457
1457
  console.log(` # --data-binary @${rel(deployPath)} https://nomos.captainapp.co.uk/v2/workspaces/<ws>/domains`);
1458
1458
  if (proofPath !== undefined) {
1459
1459
  console.log(`prove it OFFLINE first: npx githolon proof # the generated proof's offline legs on a LOCAL holon (no cloud workspace)`);