@githolon/dsl 0.105.6 → 0.105.8

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,7 @@
1
+ ## 0.64.24
2
+
3
+ - Restaged with the current kernel candidate: regenerated vendored assets (kernel release identity is baked into them). Published pub.dev archives are immutable, so regenerated bytes ride their own version.
4
+
1
5
  ## 0.64.23
2
6
 
3
7
  - Rebuild the self-contained runner without the retired structural-interface sidecar assumption;
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // <define:__NOMOS_KERNEL_RELEASE__>
4
- var define_NOMOS_KERNEL_RELEASE_default = { production: { hash: "796e512cf4549c7fadbb4e56248ec076bca3992258a1b5282d678cb56bf24f35", version: "kernel-796e512c", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 }, testMint: { hash: "beaabafa0d30eb83f1e49cc9f26251996ea3ef302e24ae36180a686145b41588", version: "kernel-beaabafa", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 } };
4
+ var define_NOMOS_KERNEL_RELEASE_default = { production: { hash: "08719339b0ecfb39b75543bf40422e84832153cc463b00283c0e84f6e38ce359", version: "kernel-08719339", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 }, testMint: { hash: "ab5c45c029a57b39711a559d803ed7d23e8eb94c11bbba652df7f8b3abd3c6df", version: "kernel-ab5c45c0", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 } };
5
5
 
6
6
  // runner/node-entry.ts
7
7
  import { createInterface } from "node:readline";
@@ -7596,7 +7596,10 @@ async function pushPack(remote, headers = {}, { updates = [], pack = null } = {}
7596
7596
  var RELEASE_RECEIPT_PREFIX = "nomos-release-receipt:";
7597
7597
  function prepareManagedPromotion(report, candidateHash, domain, opts = {}) {
7598
7598
  if (!report.sourceVerified || !report.complete || !report.evolve?.ok) {
7599
- const reason = !report.sourceVerified ? "the source ledger did not verify" : !report.complete ? "some historical premises could not be re-derived" : "the candidate fails deterministic schema evolution";
7599
+ const reason = !report.sourceVerified ? "the source ledger did not verify" : !report.complete ? "some historical premises could not be re-derived" : [
7600
+ "the candidate fails deterministic schema evolution",
7601
+ ...report.evolve.violations.map((violation) => violation.detail)
7602
+ ].join(": ");
7600
7603
  return { ok: false, stage: "preflight", report, error: `promotion refused: ${reason}` };
7601
7604
  }
7602
7605
  const acceptanceReason = opts.acceptanceReason?.trim();
@@ -8496,6 +8499,9 @@ function createSerializedScheduler() {
8496
8499
  return scheduler;
8497
8500
  }
8498
8501
 
8502
+ // src/intent_offer_codec.ts
8503
+ var encoder = new TextEncoder();
8504
+
8499
8505
  // src/index.ts
8500
8506
  var enc4 = new TextEncoder();
8501
8507
  var dec4 = new TextDecoder();
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // <define:__NOMOS_KERNEL_RELEASE__>
9
- var define_NOMOS_KERNEL_RELEASE_default = { production: { hash: "796e512cf4549c7fadbb4e56248ec076bca3992258a1b5282d678cb56bf24f35", version: "kernel-796e512c", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 }, testMint: { hash: "beaabafa0d30eb83f1e49cc9f26251996ea3ef302e24ae36180a686145b41588", version: "kernel-beaabafa", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 } };
9
+ var define_NOMOS_KERNEL_RELEASE_default = { production: { hash: "08719339b0ecfb39b75543bf40422e84832153cc463b00283c0e84f6e38ce359", version: "kernel-08719339", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 }, testMint: { hash: "ab5c45c029a57b39711a559d803ed7d23e8eb94c11bbba652df7f8b3abd3c6df", version: "kernel-ab5c45c0", features: ["compoundKeys", "capturedReads", "invariantGate", "surfacedMerge", "fieldProvenance", "nullSemantics", "attestedReads", "conflictSnapshots", "ownedAudience", "temporalEnum"], protocol: 1 } };
10
10
 
11
11
  // runner/offline-install.ts
12
12
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync } from "node:fs";
@@ -1938,7 +1938,10 @@ async function pushPack(remote, headers = {}, { updates = [], pack = null } = {}
1938
1938
  var RELEASE_RECEIPT_PREFIX = "nomos-release-receipt:";
1939
1939
  function prepareManagedPromotion(report, candidateHash, domain, opts = {}) {
1940
1940
  if (!report.sourceVerified || !report.complete || !report.evolve?.ok) {
1941
- const reason = !report.sourceVerified ? "the source ledger did not verify" : !report.complete ? "some historical premises could not be re-derived" : "the candidate fails deterministic schema evolution";
1941
+ const reason = !report.sourceVerified ? "the source ledger did not verify" : !report.complete ? "some historical premises could not be re-derived" : [
1942
+ "the candidate fails deterministic schema evolution",
1943
+ ...report.evolve.violations.map((violation) => violation.detail)
1944
+ ].join(": ");
1942
1945
  return { ok: false, stage: "preflight", report, error: `promotion refused: ${reason}` };
1943
1946
  }
1944
1947
  const acceptanceReason = opts.acceptanceReason?.trim();
@@ -5899,17 +5902,18 @@ var protoAuthority = (principal = "", keys = [], authToken = "", nowUnix = 0, au
5899
5902
  }))
5900
5903
  });
5901
5904
  function applicationQueryProto(eng2, ws, query2, options = {}) {
5905
+ const hasAuthority = options.principal !== void 0 || options.keys !== void 0 || options.authToken !== void 0 || options.nowUnix !== void 0 || options.authorSecret !== void 0;
5902
5906
  const response = callProto(eng2.ex, {
5903
5907
  case: "query",
5904
5908
  value: create(QueryRequestSchema, {
5905
5909
  target: protoTarget(ws, options.branch ?? BRANCH),
5906
- authority: protoAuthority(
5910
+ authority: hasAuthority ? protoAuthority(
5907
5911
  options.principal,
5908
5912
  options.keys,
5909
5913
  options.authToken,
5910
5914
  options.nowUnix,
5911
5915
  options.authorSecret
5912
- ),
5916
+ ) : void 0,
5913
5917
  spans: options.spans ?? false,
5914
5918
  query: query2
5915
5919
  })
@@ -7843,12 +7847,10 @@ var cryptoOwnedScopeKey = (eng2, workspace2, authorSecret, scope, principal = ""
7843
7847
  const recordObject = record === void 0 ? void 0 : `${record.type}:${record.id}`;
7844
7848
  const derived = namedQueryProto(eng2, workspace2, QueryOperation.CRYPTO_OWNED_SCOPE_KEY, {
7845
7849
  custodyWorkspace: workspace2,
7846
- principal,
7847
- authorSecret,
7848
7850
  scope,
7849
7851
  ...recordObject === void 0 ? {} : { recordObject, recordEpoch },
7850
7852
  ...recordWrap === void 0 ? {} : { recordWrap }
7851
- });
7853
+ }, { principal, authorSecret });
7852
7854
  const ownedKeySubject = String(derived.ownedKeySubject || "");
7853
7855
  if (!requireSeat) {
7854
7856
  return {
@@ -7865,7 +7867,16 @@ var cryptoOwnedScopeKey = (eng2, workspace2, authorSecret, scope, principal = ""
7865
7867
  let hex = "";
7866
7868
  for (const byte of enc3.encode(principal)) hex += byte.toString(16).padStart(2, "0");
7867
7869
  const object = `nomos-owned-principal:${hex || "00"}`;
7868
- const rows = qById(eng2, workspace2, `reltuple-${object}-nomos_internal_owned_device_key_7c1f5e`);
7870
+ const rows = qById(
7871
+ eng2,
7872
+ workspace2,
7873
+ `reltuple-${object}-nomos_internal_owned_device_key_7c1f5e`,
7874
+ principal,
7875
+ [],
7876
+ "",
7877
+ 0,
7878
+ authorSecret
7879
+ );
7869
7880
  const row = Array.isArray(rows) ? rows[0] : rows?.rows?.[0];
7870
7881
  const leaves = row?.data?.subjects ?? row?.subjects ?? {};
7871
7882
  const leaf = (raw) => {
@@ -7896,14 +7907,12 @@ var cryptoOwnedScopeKey = (eng2, workspace2, authorSecret, scope, principal = ""
7896
7907
  const custody = String(leaf(leaves[ownedKeySubject]).ownedCustody || "");
7897
7908
  if (!custody) throw new Error("the approved device has no owned-data handoff");
7898
7909
  const opened = namedQueryProto(eng2, workspace2, QueryOperation.CRYPTO_OWNED_SCOPE_KEY, {
7899
- workspace: workspace2,
7900
- principal,
7901
- authorSecret,
7910
+ custodyWorkspace: workspace2,
7902
7911
  scope,
7903
7912
  ownedCustody: custody,
7904
7913
  ...recordObject === void 0 ? {} : { recordObject, recordEpoch },
7905
7914
  ...recordWrap === void 0 ? {} : { recordWrap }
7906
- });
7915
+ }, { principal, authorSecret });
7907
7916
  return {
7908
7917
  scope: String(opened.scope || scope),
7909
7918
  epoch: Number(opened.epoch) || 0,
@@ -7924,11 +7933,8 @@ var cryptoOwnedScopeKey = (eng2, workspace2, authorSecret, scope, principal = ""
7924
7933
  };
7925
7934
  };
7926
7935
  var cryptoOwnedDeviceRequest = (eng2, workspace2, principal, authorSecret, scope) => String(namedQueryProto(eng2, workspace2, QueryOperation.CRYPTO_OWNED_DEVICE_REQUEST, {
7927
- workspace: workspace2,
7928
- principal,
7929
- authorSecret,
7930
7936
  scope
7931
- }).request || "");
7937
+ }, { principal, authorSecret }).request || "");
7932
7938
  var cryptoOwnedPrepareApproval = (eng2, args) => namedQueryProto(eng2, args.workspace, QueryOperation.CRYPTO_OWNED_PREPARE_APPROVAL, args);
7933
7939
  var cryptoOwnedPrepareRecovery = (eng2, args) => namedQueryProto(eng2, args.workspace, QueryOperation.CRYPTO_OWNED_PREPARE_RECOVERY, args);
7934
7940
  var cryptoKeygen = (eng2) => namedQueryProto(eng2, "root", QueryOperation.CRYPTO_KEYGEN);
@@ -7969,7 +7975,10 @@ var certSign = (eng2, {
7969
7975
  ...authProviderJwks !== void 0 ? { authProviderJwks } : {},
7970
7976
  ...authProviderAudience !== void 0 ? { authProviderAudience } : {}
7971
7977
  }).cert;
7972
- var cryptoWrapKey = (eng2, { recipientPub, scopeKey }) => namedQueryProto(eng2, "root", QueryOperation.CRYPTO_WRAP_KEY, { recipientPub, scopeKey });
7978
+ var cryptoWrapKey = (eng2, { recipientPub, scopeKey }) => {
7979
+ const result = namedQueryProto(eng2, "root", QueryOperation.CRYPTO_WRAP_KEY, { recipientPub, scopeKey });
7980
+ return { hpkeEpk: String(result.hpkeEpk ?? ""), ct: String(result.ct ?? "") };
7981
+ };
7973
7982
  var cryptoUnwrapKey = (eng2, { secret, hpkeEpk, ct }) => namedQueryProto(eng2, "root", QueryOperation.CRYPTO_UNWRAP_KEY, { secret, hpkeEpk, ct }).scopeKey;
7974
7983
  var readPosture = (eng2, ws, principal = "", authToken = "", nowUnix = 0, authorSecret = "") => namedQueryProto(
7975
7984
  eng2,
@@ -1254,6 +1254,14 @@ class NomosBridge {
1254
1254
  final Map<String, SyncStatus> _statusBySession = {};
1255
1255
  final Map<String, StreamController<SyncStatus>> _syncStatusBySession = {};
1256
1256
  Timer? _metricsTimer;
1257
+ // The zone this bridge was CONSTRUCTED in. The metrics poll belongs to the
1258
+ // bridge (a long-lived, often file-shared object in tests), yet it used to be
1259
+ // created in whichever zone the first status LISTENER attached from — in
1260
+ // widget tests that is a per-test FakeAsync zone, so a file-shared bridge's
1261
+ // periodic poll became a fake timer owned by ONE test and tripped
1262
+ // flutter_test's '!timersPending' invariant at that test's teardown. Timers
1263
+ // run in their owner's zone, not their caller's.
1264
+ final Zone _creationZone = Zone.current;
1257
1265
  Future<void>? _metricsPoll;
1258
1266
  bool _metricsPollAgain = false;
1259
1267
  final Completer<void> _ready = Completer<void>();
@@ -3579,8 +3587,12 @@ class NomosBridge {
3579
3587
  // metrics snapshot.
3580
3588
  if (!_syncStatusCtrl.isClosed) _syncStatusCtrl.add(_status);
3581
3589
  _pollMetricsOnce();
3582
- _metricsTimer ??=
3583
- Timer.periodic(const Duration(seconds: 4), (_) => _pollMetricsOnce());
3590
+ _metricsTimer ??= _creationZone.run(
3591
+ () => Timer.periodic(
3592
+ const Duration(seconds: 4),
3593
+ (_) => _creationZone.run(_pollMetricsOnce),
3594
+ ),
3595
+ );
3584
3596
  }
3585
3597
 
3586
3598
  void _startSessionStatusPoll(String session) {
@@ -3589,8 +3601,12 @@ class NomosBridge {
3589
3601
  controller.add(_currentSessionStatus(session));
3590
3602
  }
3591
3603
  _pollMetricsOnce();
3592
- _metricsTimer ??=
3593
- Timer.periodic(const Duration(seconds: 4), (_) => _pollMetricsOnce());
3604
+ _metricsTimer ??= _creationZone.run(
3605
+ () => Timer.periodic(
3606
+ const Duration(seconds: 4),
3607
+ (_) => _creationZone.run(_pollMetricsOnce),
3608
+ ),
3609
+ );
3594
3610
  }
3595
3611
 
3596
3612
  void _stopStatusPollIfUnused() {
package/dart/pubspec.yaml CHANGED
@@ -7,7 +7,7 @@ repository: https://github.com/Captain-App/nomos2
7
7
  description: >-
8
8
  Nomos 2 Dart client — generated domain values, intent payloads and application
9
9
  lifecycle types over the typed Nomos kernel protocol and USDA custody model.
10
- version: 0.64.23
10
+ version: 0.64.24
11
11
 
12
12
  environment:
13
13
  sdk: ^3.5.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.105.6",
3
+ "version": "0.105.8",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
@@ -6627,7 +6627,10 @@ async function pushPack(remote, headers = {}, { updates = [], pack = null } = {}
6627
6627
  var RELEASE_RECEIPT_PREFIX = "nomos-release-receipt:";
6628
6628
  function prepareManagedPromotion(report, candidateHash, domain, opts = {}) {
6629
6629
  if (!report.sourceVerified || !report.complete || !report.evolve?.ok) {
6630
- const reason = !report.sourceVerified ? "the source ledger did not verify" : !report.complete ? "some historical premises could not be re-derived" : "the candidate fails deterministic schema evolution";
6630
+ const reason = !report.sourceVerified ? "the source ledger did not verify" : !report.complete ? "some historical premises could not be re-derived" : [
6631
+ "the candidate fails deterministic schema evolution",
6632
+ ...report.evolve.violations.map((violation) => violation.detail)
6633
+ ].join(": ");
6631
6634
  return { ok: false, stage: "preflight", report, error: `promotion refused: ${reason}` };
6632
6635
  }
6633
6636
  const acceptanceReason = opts.acceptanceReason?.trim();
@@ -7527,6 +7530,9 @@ function createSerializedScheduler() {
7527
7530
  return scheduler;
7528
7531
  }
7529
7532
 
7533
+ // ../cloud/web-client/src/intent_offer_codec.ts
7534
+ var encoder = new TextEncoder();
7535
+
7530
7536
  // ../cloud/web-client/src/index.ts
7531
7537
  var enc4 = new TextEncoder();
7532
7538
  var dec4 = new TextDecoder();
package/src/aggregate.ts CHANGED
@@ -84,6 +84,7 @@ export type Audience =
84
84
  export const OWNER_FIELD = "__nomos_owner_principal_7c1f5e";
85
85
  export const OWNED_READ_RELATION = "nomos_internal_owned_reader_7c1f5e";
86
86
  export const OWNED_KEY_RELATION = "nomos_internal_owned_device_key_7c1f5e";
87
+ export const OWNED_PRINCIPAL_OBJECT_TYPE = "nomos-owned-principal";
87
88
  export const OWNED_KEY_PAYLOAD_FIELD = "__nomos_owned_device_key_subject_7c1f5e";
88
89
  export const OWNED_CUSTODY_PAYLOAD_FIELD = "__nomos_owned_device_custody_7c1f5e";
89
90
  /** Framework-carried record identities reserved by the client before an owned create is planned.
@@ -114,7 +115,7 @@ export function ownedKeyObject(principal: string): string {
114
115
  : [0xf0 | (cp >> 18), 0x80 | ((cp >> 12) & 0x3f), 0x80 | ((cp >> 6) & 0x3f), 0x80 | (cp & 0x3f)];
115
116
  for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
116
117
  }
117
- return `nomos-owned-principal:${hex || "00"}`;
118
+ return `${OWNED_PRINCIPAL_OBJECT_TYPE}:${hex || "00"}`;
118
119
  }
119
120
 
120
121
  /**
@@ -58,6 +58,8 @@ import {
58
58
  SignerDelegation,
59
59
  enrollDelegation,
60
60
  enrollSigner,
61
+ enrollSignerSelf,
62
+ enrollSignerSelfRequiresVerified,
61
63
  identityRelations,
62
64
  signerDelegationByUser,
63
65
  signerObject,
@@ -70,6 +72,7 @@ import {
70
72
  setAuthProvider,
71
73
  } from "./framework/auth_provider_law.js";
72
74
  import { refAs, workspaceInvariant } from "./framework/workspace_invariant.js";
75
+ import { isRelationTypeDecl, type RelationTypeDecl } from "./relation_schema.js";
73
76
 
74
77
  /** Lightweight copy of the framework's founding-signer guard, kept here to avoid importing the whole
75
78
  * governance module (whose unrelated captured-read laws would otherwise survive into every child lump). */
@@ -201,6 +204,34 @@ export function withBirthMeterExports<M extends Record<string, unknown>>(mod: M)
201
204
  }
202
205
  if (!found) out[key] = internal(canonical);
203
206
  };
207
+ /** Relation schemas have a typed object address rather than an `id`. Treat an explicit re-export
208
+ * of the canonical framework singleton exactly like `internalize()` treats directives: replace
209
+ * every canonical binding with one branded copy, but never annex a tenant-authored fragment that
210
+ * merely happens to extend the same object type. */
211
+ const internalizeRelationType = (key: string, canonical: RelationTypeDecl): void => {
212
+ if (hasOwn(key)) {
213
+ const existing = out[key];
214
+ const sameGeneratedType =
215
+ isGeneratedInternal(existing) &&
216
+ isRelationTypeDecl(existing) &&
217
+ existing.type === canonical.type &&
218
+ existing.global === canonical.global;
219
+ if (existing !== canonical && !sameGeneratedType) throw reservedNameError(key);
220
+ }
221
+ let found = false;
222
+ for (const [exportName, value] of Object.entries(out)) {
223
+ if (value === canonical || (
224
+ isGeneratedInternal(value) &&
225
+ isRelationTypeDecl(value) &&
226
+ value.type === canonical.type &&
227
+ value.global === canonical.global
228
+ )) {
229
+ out[exportName] = internal(value as RelationTypeDecl);
230
+ found = true;
231
+ }
232
+ }
233
+ if (!found) out[key] = internal(canonical);
234
+ };
204
235
  if (governsBirth && !declaresBirth && !acceptsBirth) {
205
236
  if (exportedDirectiveIds.has("setBirthPolicy")) internalize("__nomos_BirthPolicy", BirthPolicy);
206
237
  if (exportedDirectiveIds.has("grantBirthAllowance")) internalize("__nomos_BirthGrant", BirthGrant);
@@ -210,8 +241,14 @@ export function withBirthMeterExports<M extends Record<string, unknown>>(mod: M)
210
241
  // Carry the law needed to judge that internal step in every possible child, plus the write-once guard that
211
242
  // keeps the founding lane a genesis primitive. These declarations are certified but never tenant-facing.
212
243
  internalize("__nomos_RelationTuple", RelationTuple);
213
- reserve("__nomos_identityRelations", identityRelations);
244
+ // Keep the framework brand when this declaration is injected under a second private export key.
245
+ // Owned-custody law already carries the same signer schema under its own internal key; an unbranded
246
+ // clone makes the relation merger correctly treat it as a tenant-authored duplicate and refuse the
247
+ // package. Two framework paths naming one byte-identical relation are one declaration, not two authors.
248
+ internalizeRelationType("__nomos_identityRelations", identityRelations);
214
249
  internalize("__nomos_enrollSigner", enrollSigner);
250
+ internalize("__nomos_enrollSignerSelf", enrollSignerSelf);
251
+ internalize("__nomos_enrollSignerSelfRequiresVerified", enrollSignerSelfRequiresVerified);
215
252
  internalize("__nomos_relationTuplesByObject", relationTuplesByObject);
216
253
  internalize("__nomos_GenesisWarrant", GenesisWarrant);
217
254
  internalize("__nomos_seedGenesis", seedGenesis);
@@ -575,7 +575,7 @@ function introspectField(
575
575
  case "ZodArray": {
576
576
  const elem = zodArrayElement(zt);
577
577
  // A `string[]` is a typed `List<String>`. A list of DECLARED VALUE OBJECTS is typed
578
- // with the generated VO class (vo_authoring_end_to_end.md slice B). ANY other element
578
+ // with the generated VO class (vo_first_class.md). ANY other element
579
579
  // type is arbitrary JSON DATA to the engine, modelled as `List<Object?>`.
580
580
  if (zodTypeName(elem) === "ZodString") {
581
581
  return { name, dartType: "List<String>", toJson: "strList" };
@@ -239,7 +239,7 @@ function sampleOfZod(
239
239
  next.add(zt);
240
240
  return sampleOfZod(zodUnwrapLazy(zt), path, where, voIncludeOptional, next);
241
241
  }
242
- // VALUE OBJECTS FIRST (vo_authoring_end_to_end.md slice C): a declared VO carries its own lawful
242
+ // VALUE OBJECTS FIRST (vo_first_class.md): a declared VO carries its own lawful
243
243
  // sample (the author's validated `example`, the blessed geometry sample, or per-field descriptor
244
244
  // synthesis, recursing through nested VOs / arrays-of-VOs / unions). The raw-zod walk below cannot
245
245
  // see interior constraints (geo's min-2 coordinates, a `.min()` on a subfield), so after building the
package/src/codegen_ts.ts CHANGED
@@ -333,7 +333,7 @@ function tsTypeOfZod(
333
333
  const target = idOfTargetKindOf(zt) === "contract" ? contractTypeName(aggregateType) : aggregateType;
334
334
  return `NomosRef<${target}>`;
335
335
  }
336
- // A DECLARED VALUE OBJECT keeps its NAME (vo_authoring_end_to_end.md slice B): the payload
336
+ // A DECLARED VALUE OBJECT keeps its NAME (vo_first_class.md): the payload
337
337
  // references the same exported interface the read model uses — one name, both directions —
338
338
  // instead of an anonymous inline literal type. Checked on the BARE object only: optional/
339
339
  // default wrappers must keep flowing through the switch so `| undefined` survives.
@@ -761,6 +761,9 @@ export interface TsClientOptions {
761
761
  /** Explicit application home for direct/synthetic generation. The package compiler derives this from
762
762
  * business birth topology and passes it here; ambiguous multi-domain packages fail rather than flatten. */
763
763
  readonly homeDomain?: string;
764
+ /** Emit the structural holon factory for the compiler-owned headless artifact. The ordinary tenant
765
+ * client leaves this false so framework transport types never enter its public declaration graph. */
766
+ readonly exportHolonFactory?: boolean;
764
767
  }
765
768
 
766
769
  /** TS string literal (JSON.stringify covers escaping). */
@@ -1890,9 +1893,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1890
1893
  ` /** THE ACTIVITY FEED (declared \`.activity(...)\`): subscribe to per-committed-intent RAW ROWS`,
1891
1894
  ` * (the runtime folds the intent log). \`cb\` fires with the current rows + on every new intent;`,
1892
1895
  ` * returns an unsubscribe fn. The generated client maps these through \`renderActivity\`. */`,
1893
- ` watchActivity(cb: (rows: ActivityRawRow[]) => void, opts?: { base?: string; intervalMs?: number }): () => void;`,
1896
+ ` watchActivity?(cb: (rows: ActivityRawRow[]) => void, opts?: { base?: string; intervalMs?: number }): () => void;`,
1894
1897
  ` /** One-shot: the current committed activity log as RAW ROWS (folded to typed rows by the client). */`,
1895
- ` activityLog(opts?: { base?: string; limit?: number }): Promise<ActivityRawRow[]>;`,
1898
+ ` activityLog?(opts?: { base?: string; limit?: number }): Promise<ActivityRawRow[]>;`,
1896
1899
  ]
1897
1900
  : []),
1898
1901
  ` /** THE COMPAT SUPERPOWER (#72b) — OPTIONAL: a holon from @githolon/client's connect() attaches it.`,
@@ -2090,6 +2093,25 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2090
2093
  ` return read(keys);`,
2091
2094
  `};`,
2092
2095
  ``,
2096
+ `/** Owned bounded reads perform the same record-key-aware second pass without erasing`,
2097
+ ` * the page envelope. Keeping this distinct from the array helper makes cursor semantics explicit. */`,
2098
+ `const _readPageWithOwnedKeys = async <T>(`,
2099
+ ` holon: NomosHolon,`,
2100
+ ` keyStore: NomosKeyStore | undefined,`,
2101
+ ` scopes: readonly { scope: string; owned?: true }[],`,
2102
+ ` read: (keys: readonly NomosScopeKey[] | undefined) => Promise<{ rows: T[]; cursor: string | null; done: boolean }>,`,
2103
+ `): Promise<{ rows: T[]; cursor: string | null; done: boolean }> => {`,
2104
+ ` const firstKeys = await _readKeys(holon, keyStore, scopes);`,
2105
+ ` const first = await read(firstKeys);`,
2106
+ ` if (!scopes.some((scope) => scope.owned === true) || first.rows.length === 0) return first;`,
2107
+ ` const records = first.rows.map((row) => {`,
2108
+ ` const raw = row as T & { type: string; id: string };`,
2109
+ ` return { type: raw.type, id: raw.id };`,
2110
+ ` });`,
2111
+ ` const keys = await _readKeys(holon, keyStore, scopes, records);`,
2112
+ ` return read(keys);`,
2113
+ `};`,
2114
+ ``,
2093
2115
  `/** Reactive owned reads use the exact same two-pass, record-key-aware read as one-shot reads.`,
2094
2116
  ` * The raw watch is only a local change signal; ciphertext rows never reach application callbacks. */`,
2095
2117
  `const _watchWithOwnedKeys = <T>(`,
@@ -3375,7 +3397,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
3375
3397
  );
3376
3398
  const queryPageCall = qScopes.length
3377
3399
  ? qOwned
3378
- ? `_readWithOwnedKeys(holon, keyStore, ${qScopeJson}, (keys) => (holon.queryPage ?? (() => { throw new Error("bounded reads need a current @githolon/client"); }))(${JSON.stringify(q.id)}, ${wireParams}, page, keys) as Promise<{ rows: Row<${q.returns}>[]; cursor: string | null; done: boolean }>)`
3400
+ ? `_readPageWithOwnedKeys(holon, keyStore, ${qScopeJson}, (keys) => (holon.queryPage ?? (() => { throw new Error("bounded reads need a current @githolon/client"); }))(${JSON.stringify(q.id)}, ${wireParams}, page, keys) as Promise<{ rows: Row<${q.returns}>[]; cursor: string | null; done: boolean }>)`
3379
3401
  : `(holon.queryPage ?? (() => { throw new Error("bounded reads need a current @githolon/client"); }))(${JSON.stringify(q.id)}, ${wireParams}, page, await _readKeys(holon, keyStore, ${qScopeJson})) as Promise<{ rows: Row<${q.returns}>[]; cursor: string | null; done: boolean }>`
3380
3402
  : `(holon.queryPage ?? (() => { throw new Error("bounded reads need a current @githolon/client"); }))(${JSON.stringify(q.id)}, ${wireParams}, page) as Promise<{ rows: Row<${q.returns}>[]; cursor: string | null; done: boolean }>`;
3381
3403
  methods.push(
@@ -3639,15 +3661,15 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
3639
3661
  ` * Every public directive has a baseline; \`.activity(...)\` enriches it. Framework/control work`,
3640
3662
  ` * outside this generated package is dropped. Returns stop(). */`,
3641
3663
  ` watchActivity: (cb: (events: ActivityEvent[]) => void, opts?: { base?: string }) =>`,
3642
- ` holon.watchActivity((rows) => cb(rows.map(renderActivity).filter((e): e is ActivityEvent => e !== null)), opts),`,
3664
+ ` _nomosWatchActivity((rows) => cb(rows.map(renderActivity).filter((e): e is ActivityEvent => e !== null)), opts),`,
3643
3665
  ` /** One-shot: the current committed activity log, folded to typed [ActivityEvent]s. */`,
3644
3666
  ` activityLog: async (opts?: { base?: string; limit?: number }): Promise<ActivityEvent[]> =>`,
3645
- ` (await holon.activityLog(opts)).map(renderActivity).filter((e): e is ActivityEvent => e !== null),`,
3667
+ ` (await _nomosActivityLog(opts)).map(renderActivity).filter((e): e is ActivityEvent => e !== null),`,
3646
3668
  ` /** THE SELF-DESCRIPTION: root links to exactly six grounded pages — model, current state, rules,`,
3647
3669
  ` * history, decisions and evidence. The old \`events\` / \`transcript\` fields remain, so this is a`,
3648
3670
  ` * strict enrichment of the activity narrative. \`section\` selects one page. */`,
3649
3671
  ` describe: async (opts: { section?: NomosDescribeSection; since?: string; performedBy?: string; category?: string; limit?: number } = {}): Promise<NomosDescription> => {`,
3650
- ` const raw = await holon.activityLog(opts.limit !== undefined ? { limit: opts.limit } : {});`,
3672
+ ` const raw = await _nomosActivityLog(opts.limit !== undefined ? { limit: opts.limit } : {});`,
3651
3673
  ` let events = raw.map(renderActivity).filter((e): e is ActivityEvent => e !== null);`,
3652
3674
  ` if (opts.performedBy !== undefined) events = events.filter((e) => e.performedBy === opts.performedBy);`,
3653
3675
  ` if (opts.category !== undefined) events = events.filter((e) => e.category === opts.category);`,
@@ -3759,7 +3781,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
3759
3781
  ` * point at it (only directives declaring a ref to the aggregate appear in its story) + a transcript. */`,
3760
3782
  ` describeHistory: async <TAggregate>(record: NomosRef<TAggregate>, opts: { limit?: number } = {}): Promise<ActivityNarrative> => {`,
3761
3783
  ` const aggregateId = _nmsReferenceId(record, "", holon.workspace, "record");`,
3762
- ` const raw = await holon.activityLog(opts.limit !== undefined ? { limit: opts.limit } : {});`,
3784
+ ` const raw = await _nomosActivityLog(opts.limit !== undefined ? { limit: opts.limit } : {});`,
3763
3785
  ` const events = raw.map(renderActivity).filter((e): e is ActivityEvent => e !== null)`,
3764
3786
  ` .filter((e) => Object.values(e.refs).some((r) => _nmsReferenceData(r).id === aggregateId));`,
3765
3787
  ` return { events, transcript: _activityTranscript(events) };`,
@@ -3770,9 +3792,19 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
3770
3792
  out.push(
3771
3793
  `/** Framework binding for the generated application. Law identity, author identity and key custody`,
3772
3794
  ` * are captured from the connected stateful client; callers cannot override them. */`,
3773
- `function ${tsClientFactoryName(domain)}(holon: NomosHolon) {`,
3795
+ `/** THE HOLON-LEVEL FACTORY — the headless/code-mode binding (headless-operation.md). Binds`,
3796
+ ` * structurally to any NomosHolon: a full \`connect()\` holon, or the thin remote holon`,
3797
+ ` * (\`connectRemote\`) for isolates that live beside the edge. The application surface`,
3798
+ ` * (\`open<Name>Application\`) remains the batteries-included entry for devices. */`,
3799
+ `${opts.exportHolonFactory === true ? "export " : ""}function ${tsClientFactoryName(domain)}(holon: NomosHolon) {`,
3774
3800
  ` const domainHash = ${hashConst};`,
3775
3801
  ` const actor = holon.principal;`,
3802
+ ...(activityPresent
3803
+ ? [
3804
+ ` const _nomosActivityLog = (opts?: { base?: string; limit?: number }) => { if (!holon.activityLog) throw new Error("activity reads need a custody-capable @githolon/client binding"); return holon.activityLog(opts); };`,
3805
+ ` const _nomosWatchActivity = (cb: (rows: ActivityRawRow[]) => void, opts?: { base?: string; intervalMs?: number }) => { if (!holon.watchActivity) throw new Error("activity watches need a custody-capable @githolon/client binding"); return holon.watchActivity(cb, opts); };`,
3806
+ ]
3807
+ : []),
3776
3808
  ` holon.prepareDomain?.(${JSON.stringify(domain)}, domainHash);`,
3777
3809
  ...(encPresent ? [` const keyStore: NomosKeyStore | undefined = ${domScopes.length > 0 ? `{ scopeKey: async (scope) => { if (!holon.__nomosScopeKey) throw new Error("encrypted business data needs the current @githolon/client"); return holon.__nomosScopeKey({ scope }); }, readKeys: async (scopes) => { if (!holon.__nomosScopeKeys) throw new Error("encrypted business data needs the current @githolon/client"); return holon.__nomosScopeKeys({ scopes }); } }` : "undefined"};`] : []),
3778
3810
  ...(ownedScope !== undefined && Object.keys(ownedActions).length > 0
@@ -1,4 +1,4 @@
1
- // The CLIENT/LAW COMPATIBILITY MANIFEST (§5, architecture/plug_and_play_dx.md).
1
+ // The CLIENT/LAW COMPATIBILITY MANIFEST (architecture/shape_compatibility.md).
2
2
  //
3
3
  // A DIAGNOSIS surface, NEVER a gate. Clients fit workspaces by STRUCTURAL INTERFACE
4
4
  // (stable-id subtyping, #72), not by law-hash pin — the hash pin was a bug we removed.
@@ -1660,8 +1660,8 @@ const NOMOS_TYPES_DEP = "^0.8.7";
1660
1660
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1661
1661
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1662
1662
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1663
- const NOMOS_CLIENT_DEP = "^0.64.23";
1664
- const NOMOS_FLUTTER_DEP = "^0.63.44";
1663
+ const NOMOS_CLIENT_DEP = "^0.64.24";
1664
+ const NOMOS_FLUTTER_DEP = "^0.63.46";
1665
1665
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1666
1666
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1667
1667
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -5080,7 +5080,7 @@ async function main(): Promise<void> {
5080
5080
  identityHashes[module.domain ?? module.name] = hash;
5081
5081
  }
5082
5082
 
5083
- // ── 3a. the CLIENT/LAW COMPATIBILITY MANIFEST (§5, architecture/plug_and_play_dx.md).
5083
+ // ── 3a. the CLIENT/LAW COMPATIBILITY MANIFEST (architecture/shape_compatibility.md).
5084
5084
  // A DIAGNOSIS surface, NEVER a gate: the shape-compat arc (#72) established that clients
5085
5085
  // fit workspaces by STRUCTURAL INTERFACE (stable-id subtyping), not by law-hash pin. This
5086
5086
  // block is baked into both generated clients (TS + Dart) and mirrored into the deploy JSON
@@ -5253,20 +5253,29 @@ async function main(): Promise<void> {
5253
5253
  // deployed law's content hash baked in (emitted AFTER the package is hashed) ──
5254
5254
  const clientPath = path.join(outDir, `${cfg.name}.client.mjs`);
5255
5255
  const clientDeclarationPath = path.join(outDir, `${cfg.name}.client.d.mts`);
5256
- const clientSource = generateTsClient(presentationDomainModules, {
5256
+ const headlessClientPath = path.join(outDir, `${cfg.name}.headless.mjs`);
5257
+ const headlessClientDeclarationPath = path.join(outDir, `${cfg.name}.headless.d.mts`);
5258
+ const clientOptions = {
5257
5259
  packageName: cfg.name,
5258
5260
  domainHash,
5259
5261
  interface: packageInterface,
5260
5262
  compat: nomosCompat,
5261
5263
  homeDomain: resolveApplicationTopology(presentationDomainModules).homeDomain,
5264
+ } as const;
5265
+ const clientSource = generateTsClient(presentationDomainModules, clientOptions);
5266
+ const headlessClientSource = generateTsClient(presentationDomainModules, {
5267
+ ...clientOptions,
5268
+ exportHolonFactory: true,
5262
5269
  });
5263
5270
  const runtimePath = path.join(DSL_DIR, "runtime", "generated-application-host.js");
5264
5271
  if (!existsSync(runtimePath)) {
5265
5272
  fail("the compiler-private generated application runtime is missing — reinstall @githolon/dsl");
5266
5273
  }
5267
5274
  const temporaryClientPath = path.join(outDir, `.${cfg.name}.client.source.ts`);
5275
+ const temporaryHeadlessClientPath = path.join(outDir, `.${cfg.name}.headless.source.ts`);
5268
5276
  const temporaryRuntimeTypesPath = path.join(outDir, `.${cfg.name}.application-runtime.d.ts`);
5269
5277
  const temporaryDeclarationPath = temporaryClientPath.replace(/\.ts$/, ".d.ts");
5278
+ const temporaryHeadlessDeclarationPath = temporaryHeadlessClientPath.replace(/\.ts$/, ".d.ts");
5270
5279
  const runtimeTypesSource = [
5271
5280
  `export declare function nomosInternalOpenGeneratedApplication<T extends object>(`,
5272
5281
  ` options: { cloud: string; application: string; signIn: (request?: { readonly deviceChallenge?: string }) => Promise<string>; onPerformance?: (sample: { readonly operation: "open" | "offer" | "birth" | "read" | "sync" | "maintenance"; readonly name: string; readonly durationMs: number; readonly domain?: string; readonly directive?: string }) => void; lawHash: string; readyCommands?: readonly { readonly domain: string; readonly packageHash: string; readonly directiveId: string }[] },`,
@@ -5306,6 +5315,8 @@ async function main(): Promise<void> {
5306
5315
  .update(`${declarationProofFormat}\0`)
5307
5316
  .update(normalizedClientSource)
5308
5317
  .update("\0")
5318
+ .update(headlessClientSource.replaceAll(domainHash, "<nomos-composed-law-identity>"))
5319
+ .update("\0")
5309
5320
  .update(runtimeTypesSource)
5310
5321
  .update("\0")
5311
5322
  .update(ts.version)
@@ -5313,25 +5324,37 @@ async function main(): Promise<void> {
5313
5324
  .update(readFileSync(fileURLToPath(import.meta.url)))
5314
5325
  .digest("hex");
5315
5326
  const declarationCacheDir = path.join(cfgDir, "node_modules", ".cache", "nomos", "typescript-declarations");
5316
- const declarationCachePath = path.join(declarationCacheDir, `${declarationInputIdentity}.d.mts`);
5317
- const declarationCacheProofPath = path.join(declarationCacheDir, `${declarationInputIdentity}.proof.usda`);
5318
- const declarationProofKey = (bytes: Buffer): string => createHash("sha256")
5319
- .update(`${declarationProofFormat}\0${declarationInputIdentity}\0`)
5327
+ const declarationCachePath = path.join(declarationCacheDir, `${declarationInputIdentity}.client.d.mts`);
5328
+ const headlessDeclarationCachePath = path.join(declarationCacheDir, `${declarationInputIdentity}.headless.d.mts`);
5329
+ const declarationCacheProofPath = path.join(declarationCacheDir, `${declarationInputIdentity}.client.proof.usda`);
5330
+ const headlessDeclarationCacheProofPath = path.join(declarationCacheDir, `${declarationInputIdentity}.headless.proof.usda`);
5331
+ const declarationProofKey = (artifact: "client" | "headless", bytes: Buffer): string => createHash("sha256")
5332
+ .update(`${declarationProofFormat}\0${declarationInputIdentity}\0${artifact}\0`)
5320
5333
  .update(bytes)
5321
5334
  .digest("hex");
5322
5335
  const cachedDeclaration = existsSync(declarationCachePath) ? readFileSync(declarationCachePath) : undefined;
5336
+ const cachedHeadlessDeclaration = existsSync(headlessDeclarationCachePath)
5337
+ ? readFileSync(headlessDeclarationCachePath)
5338
+ : undefined;
5323
5339
  const declarationCacheHit = cachedDeclaration !== undefined &&
5340
+ cachedHeadlessDeclaration !== undefined &&
5324
5341
  declarationProofCodec.readKeyProofUsda(declarationCacheProofPath, declarationProofFormat) ===
5325
- declarationProofKey(cachedDeclaration);
5326
- if (declarationCacheHit) copyFileSync(declarationCachePath, clientDeclarationPath);
5342
+ declarationProofKey("client", cachedDeclaration) &&
5343
+ declarationProofCodec.readKeyProofUsda(headlessDeclarationCacheProofPath, declarationProofFormat) ===
5344
+ declarationProofKey("headless", cachedHeadlessDeclaration);
5345
+ if (declarationCacheHit) {
5346
+ copyFileSync(declarationCachePath, clientDeclarationPath);
5347
+ copyFileSync(headlessDeclarationCachePath, headlessClientDeclarationPath);
5348
+ }
5327
5349
  try {
5328
5350
  if (!declarationCacheHit) {
5329
5351
  writeFileSync(temporaryClientPath, clientSource, "utf8");
5352
+ writeFileSync(temporaryHeadlessClientPath, headlessClientSource, "utf8");
5330
5353
  writeFileSync(temporaryRuntimeTypesPath, runtimeTypesSource, "utf8");
5331
5354
  const typescriptEntry = createRequire(import.meta.url).resolve("typescript/lib/typescript.js");
5332
5355
  const ts = (await import(pathToFileURL(typescriptEntry).href)) as typeof import("typescript");
5333
5356
  const program = ts.createProgram(
5334
- [temporaryClientPath, temporaryRuntimeTypesPath],
5357
+ [temporaryClientPath, temporaryHeadlessClientPath, temporaryRuntimeTypesPath],
5335
5358
  {
5336
5359
  target: ts.ScriptTarget.ES2022,
5337
5360
  module: ts.ModuleKind.NodeNext,
@@ -5351,7 +5374,10 @@ async function main(): Promise<void> {
5351
5374
  );
5352
5375
  const emit = program.emit();
5353
5376
  const diagnostics = [...ts.getPreEmitDiagnostics(program), ...emit.diagnostics];
5354
- if (diagnostics.length > 0 || emit.emitSkipped || !existsSync(temporaryDeclarationPath)) {
5377
+ if (
5378
+ diagnostics.length > 0 || emit.emitSkipped ||
5379
+ !existsSync(temporaryDeclarationPath) || !existsSync(temporaryHeadlessDeclarationPath)
5380
+ ) {
5355
5381
  const detail = diagnostics.map((diagnostic) => {
5356
5382
  const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
5357
5383
  if (diagnostic.file === undefined || diagnostic.start === undefined) return message;
@@ -5497,14 +5523,23 @@ async function main(): Promise<void> {
5497
5523
  ].join("\n");
5498
5524
  writeFileSync(temporaryDeclarationPath, declarationSource, "utf8");
5499
5525
  renameSync(temporaryDeclarationPath, clientDeclarationPath);
5526
+ renameSync(temporaryHeadlessDeclarationPath, headlessClientDeclarationPath);
5500
5527
  mkdirSync(declarationCacheDir, { recursive: true });
5501
5528
  const declarationCacheTemporaryPath = `${declarationCachePath}.tmp-${process.pid}`;
5529
+ const headlessDeclarationCacheTemporaryPath = `${headlessDeclarationCachePath}.tmp-${process.pid}`;
5502
5530
  copyFileSync(clientDeclarationPath, declarationCacheTemporaryPath);
5531
+ copyFileSync(headlessClientDeclarationPath, headlessDeclarationCacheTemporaryPath);
5503
5532
  renameSync(declarationCacheTemporaryPath, declarationCachePath);
5533
+ renameSync(headlessDeclarationCacheTemporaryPath, headlessDeclarationCachePath);
5504
5534
  declarationProofCodec.writeKeyProofUsda(
5505
5535
  declarationCacheProofPath,
5506
5536
  declarationProofFormat,
5507
- declarationProofKey(readFileSync(declarationCachePath)),
5537
+ declarationProofKey("client", readFileSync(declarationCachePath)),
5538
+ );
5539
+ declarationProofCodec.writeKeyProofUsda(
5540
+ headlessDeclarationCacheProofPath,
5541
+ declarationProofFormat,
5542
+ declarationProofKey("headless", readFileSync(headlessDeclarationCachePath)),
5508
5543
  );
5509
5544
  }
5510
5545
  await esbuildMod.build({
@@ -5540,12 +5575,44 @@ async function main(): Promise<void> {
5540
5575
  ) {
5541
5576
  fail("generated TypeScript application did not seal its private runtime binding");
5542
5577
  }
5578
+ await esbuildMod.build({
5579
+ stdin: {
5580
+ contents: headlessClientSource,
5581
+ loader: "ts",
5582
+ resolveDir: outDir,
5583
+ sourcefile: `${cfg.name}.headless.ts`,
5584
+ },
5585
+ outfile: headlessClientPath,
5586
+ bundle: true,
5587
+ packages: "bundle",
5588
+ platform: "node",
5589
+ format: "esm",
5590
+ target: "node20",
5591
+ legalComments: "none",
5592
+ logLevel: "silent",
5593
+ plugins: [{
5594
+ name: "nomos-generated-headless-runtime",
5595
+ setup(build) {
5596
+ build.onResolve(
5597
+ { filter: /^@githolon\/client\/_generated_application_host_7c1f5e$/ },
5598
+ () => ({ path: runtimePath }),
5599
+ );
5600
+ },
5601
+ }],
5602
+ });
5603
+ const headlessBundled = canonicalizeGeneratedClientJavascript(readFileSync(headlessClientPath, "utf8"));
5604
+ writeFileSync(headlessClientPath, headlessBundled, "utf8");
5605
+ if (!new RegExp(`export\\s*\\{[^}]*${tsClientFactoryName(resolveApplicationTopology(presentationDomainModules).homeDomain)}`, "s").test(headlessBundled)) {
5606
+ fail("generated headless application omitted its structural holon factory");
5607
+ }
5543
5608
  } catch (e) {
5544
5609
  fail(`generated TypeScript application could not be sealed — this is a Nomos codegen bug: ${(e as Error).message}`);
5545
5610
  } finally {
5546
5611
  rmSync(temporaryClientPath, { force: true });
5612
+ rmSync(temporaryHeadlessClientPath, { force: true });
5547
5613
  rmSync(temporaryRuntimeTypesPath, { force: true });
5548
5614
  rmSync(temporaryDeclarationPath, { force: true });
5615
+ rmSync(temporaryHeadlessDeclarationPath, { force: true });
5549
5616
  rmSync(path.join(outDir, `${cfg.name}.client.ts`), { force: true });
5550
5617
  }
5551
5618
 
@@ -5903,6 +5970,7 @@ async function main(): Promise<void> {
5903
5970
  }
5904
5971
  }
5905
5972
  console.log(` client ${rel(clientPath)} (typed TS client — payloads, read models, query accessors)`);
5973
+ console.log(` headless ${rel(headlessClientPath)} + ${rel(headlessClientDeclarationPath)} (typed business binding for code-mode/edge holons)`);
5906
5974
  for (const note of recursiveNotes) console.log(` note ${note}`);
5907
5975
  console.log(` openapi served by the holon from installed USDA (GET /v2/workspaces/<ws>/openapi.json) — no build-time copy to drift`);
5908
5976
  if (proofPath !== undefined) {
@@ -96,12 +96,11 @@ export const ReceivingGroup = aggregate("NomosInternalReceivingGroup7c1f5e", {
96
96
  * and secure: a live, verified device may add or revoke another device; keyless IdP enrolment is only the
97
97
  * pre-group bootstrap lane.
98
98
  *
99
- * ALL-DEVICE-LOSS RECOVERY — design done, NOT YET SHIPPED (`architecture/guardian_device_recovery.md`,
100
- * 2026-08-06). DO NOT re-derive this from scratch under a live incident again — read that doc first. The
101
- * primitives already exist (`enrollDelegation` + `birthReceivingGroup` + `revokeSigner`); what's missing is
102
- * a kernel-side closure (birth-repeat currently accepts a SELF-signed delegation onto an already-founded
103
- * group — a live gap, not a feature, until closed) plus client wiring. `guardianQuorum`/`recovery` below are
104
- * the policy fields that design reads.
99
+ * ALL-DEVICE-LOSS RECOVERY — CO2 already recovers through its platform AuthProvider, but the implementation
100
+ * reaches that result by repeating birth after this home's direct enrollment policy refuses. The corrected
101
+ * design (`architecture/guardian_device_recovery.md`, reconciled 2026-08-10) makes recovery authority an
102
+ * explicit tenant-selected policy: platform identity provider or guardian quorum. The missing work is to
103
+ * evaluate that choice at the existing child's frontier and delete the client-side birth-repeat bypass.
105
104
  */
106
105
  export const ReceivingGroupPolicy = aggregate("NomosInternalReceivingGroupPolicy7c1f5e", {
107
106
  additionalDevice: t.string().merge(Lww),
@@ -207,15 +206,15 @@ const receivingGroupPolicyRefs = readSet(
207
206
  );
208
207
 
209
208
  /** An IdP token may bootstrap the parent before the group exists; it may never bypass an existing group's
210
- * device policy. A new group device must be approved by a current device or, later, guardian evidence.
209
+ * device policy. An existing group chooses whether recovery is delegated to its platform AuthProvider or to
210
+ * guardians; the current default below says guardian, while CO2's current product behaviour is platform IdP.
211
211
  *
212
212
  * KNOWN GAP (`architecture/guardian_device_recovery.md`): this invariant is declared `.on("enrollFirstDevice")`
213
213
  * and only fires when that directive targets THIS home directly. It is NOT re-applied when the birth
214
214
  * offer-effect (`run_birth_effect`, repeating `nomosInternalBirthReceivingGroup` against an ALREADY-founded
215
- * home) folds a signer seat via the SAME underlying mechanism — a self-signed keyless delegation currently
216
- * reseats a device there with no guardian, no approval, no delay. Verified live 2026-08-06. Fix belongs in
217
- * the kernel's keyless `verify_to_root` branch (refuse a self-signed cert onto an already-warranted signer
218
- * roster); do not paper over it in the DSL only. */
215
+ * home) folds a signer seat via the SAME underlying mechanism. That is how CO2 recovery works today, but its
216
+ * home frontier still says `guardian-quorum`. The fix is not to ban platform recovery: add the explicit
217
+ * platform-identity-provider policy, make CO2 choose it, and have the child gate verify the chosen evidence. */
219
218
  export const receivingGroupRejectsKeylessAdditionalDevice = workspaceInvariant("ReceivingGroupRejectsKeylessAdditionalDevice")
220
219
  .on("enrollFirstDevice")
221
220
  .reads(receivingGroupPolicyRefs)
@@ -2,11 +2,11 @@
2
2
  // Tenant code sees only generated `devices.request()` / `devices.approve(request)`.
3
3
 
4
4
  import { z } from "zod";
5
- import { OWNED_KEY_RELATION, OWNED_READ_RELATION, aggregate, instance, ownedKeyObject, type AggregateHandle } from "../aggregate.js";
5
+ import { OWNED_KEY_RELATION, OWNED_PRINCIPAL_OBJECT_TYPE, OWNED_READ_RELATION, aggregate, instance, ownedKeyObject, type AggregateHandle } from "../aggregate.js";
6
6
  import { Lww } from "../drivers.js";
7
7
  import { directive } from "../directive.js";
8
8
  import { endorsementOf } from "../principal_evidence.js";
9
- import { isRelationTypeDecl } from "../relation_schema.js";
9
+ import { isRelationTypeDecl, key, relationType, self, user } from "../relation_schema.js";
10
10
  import { t } from "../fields.js";
11
11
  import { set, withMarker } from "../ops.js";
12
12
  import { query } from "../query.js";
@@ -19,6 +19,7 @@ import {
19
19
  writeWrap,
20
20
  } from "./encryption_scope.js";
21
21
  import { GenesisWarrant, seedGenesis } from "./genesis.js";
22
+ import { BirthAttestation, seedBirthAttestation } from "./home.js";
22
23
  import { RelationTuple, __qualifyAggregateType, removeTuple, writeTuple } from "./relations.js";
23
24
  import { SIGNER_RELATION, enrollSigner, signerObject, identityRelations } from "./signing_identity.js";
24
25
  import { impureCapability, impureCapabilityAggregate } from "./impure_capability.js";
@@ -27,6 +28,10 @@ import { impureCapability, impureCapabilityAggregate } from "./impure_capability
27
28
  export const OWNED_DEVICE_APPROVAL_DIRECTIVE = "nomos_internal_approve_owned_device_7c1f5e";
28
29
  export const OWNED_INITIALIZE_DIRECTIVE = "nomos_internal_initialize_owned_identity_7c1f5e";
29
30
  export const OWNED_RECOVERY_RELATION = "nomos_internal_owned_recovery_principal_7c1f5e";
31
+ export const ownedCustodyRelations = relationType(OWNED_PRINCIPAL_OBJECT_TYPE)
32
+ .asGlobal()
33
+ .relation(OWNED_KEY_RELATION, { rewrite: self(), allow: [key()] })
34
+ .relation(OWNED_RECOVERY_RELATION, { rewrite: self(), allow: [user()] });
30
35
  export const OWNED_RECOVERY_PROTECT_DIRECTIVE = "nomos_internal_protect_owned_identity_7c1f5e";
31
36
  export const OWNED_RECOVERY_REQUEST_DIRECTIVE = "nomos_internal_request_owned_recovery_7c1f5e";
32
37
  export const OWNED_RECOVERY_COMPLETE_DIRECTIVE = "nomos_internal_complete_owned_recovery_7c1f5e";
@@ -109,7 +114,33 @@ const ownedRecoveryLifecycle = impureCapability({
109
114
  writeReceipt: (id, r) => [
110
115
  set(instance(OwnedRecoveryOrder, id), "ownedCustody", r.ownedCustody as string),
111
116
  ],
117
+ // This is intentionally unaddressed: opening the designated principal's ordinary generated
118
+ // client is the provider. Declaring the capability still matters because every durable Order must
119
+ // have one typed provider contract; the kernel may then retain the sealed obligation locally until
120
+ // that principal opens the workspace, rather than inventing an untyped host work description.
121
+ capability: "nomos.owned-recovery",
112
122
  });
123
+ const hiddenRecoveryLifecycle = {
124
+ aggregate: hidden(OwnedRecoveryOrder),
125
+ obligation: hidden(ownedRecoveryLifecycle.obligation),
126
+ complete: hidden(ownedRecoveryLifecycle.complete),
127
+ fail: hidden(ownedRecoveryLifecycle.fail),
128
+ block: hidden(ownedRecoveryLifecycle.block),
129
+ deadLetter: hidden(ownedRecoveryLifecycle.deadLetter),
130
+ };
131
+ const ownedRecoveryCapability = {
132
+ ...ownedRecoveryLifecycle,
133
+ // Capability expansion deliberately re-seats the lifecycle quartet after application-language
134
+ // closure. Carry the private declarations there too; pointing back at the raw impure-capability
135
+ // declarations silently reintroduced framework verbs into proof selection and generated names.
136
+ aggregate: hiddenRecoveryLifecycle.aggregate,
137
+ obligation: hiddenRecoveryLifecycle.obligation,
138
+ complete: hiddenRecoveryLifecycle.complete,
139
+ fail: hiddenRecoveryLifecycle.fail,
140
+ block: hiddenRecoveryLifecycle.block,
141
+ deadLetter: hiddenRecoveryLifecycle.deadLetter,
142
+ __isCapabilityDecl: true as const,
143
+ };
113
144
 
114
145
  export const ownedRecoveryOrdersByRecoveryPrincipal = query("nomosOwnedRecoveryOrdersByRecoveryPrincipal")
115
146
  .key("recoveryPrincipal")
@@ -157,7 +188,7 @@ const protectOwnedIdentity = Object.assign(
157
188
  .alsoEnsures(RelationTuple)
158
189
  .alsoEnsures(KeyScope)
159
190
  .alsoEnsures(WrappedKey),
160
- { businessBirthInternal: true as const },
191
+ { businessBirthInternal: true as const, ownedAuthoringInternal: true as const },
161
192
  );
162
193
 
163
194
  const acceptOwnedRecovery = Object.assign(
@@ -181,18 +212,9 @@ const acceptOwnedRecovery = Object.assign(
181
212
  ownedCustody: p.ownedCustody,
182
213
  });
183
214
  }),
184
- { businessBirthInternal: true as const },
215
+ { businessBirthInternal: true as const, ownedAuthoringInternal: true as const },
185
216
  );
186
217
 
187
- const hiddenRecoveryLifecycle = {
188
- aggregate: hidden(OwnedRecoveryOrder),
189
- obligation: hidden(ownedRecoveryLifecycle.obligation),
190
- complete: hidden(ownedRecoveryLifecycle.complete),
191
- fail: hidden(ownedRecoveryLifecycle.fail),
192
- block: hidden(ownedRecoveryLifecycle.block),
193
- deadLetter: hidden(ownedRecoveryLifecycle.deadLetter),
194
- };
195
-
196
218
  export type OwnedRecordAction = "share" | "unshare" | "transfer";
197
219
 
198
220
  /** One collision-resistant framework id per aggregate/action. Tenant code never sees these ids. */
@@ -256,6 +278,7 @@ const approval = directive(OWNED_DEVICE_APPROVAL_DIRECTIVE)
256
278
  /** Branded internal so ordinary directive codegen never exposes its cryptographic payload. */
257
279
  export const approveOwnedDevice = Object.assign(approval, {
258
280
  businessBirthInternal: true as const,
281
+ ownedAuthoringInternal: true as const,
259
282
  });
260
283
 
261
284
  /** Establish this principal's shared owned-data root before another principal shares to them. */
@@ -281,7 +304,7 @@ const initializeOwnedIdentity = Object.assign(
281
304
  ownedCustody: p.ownedCustody,
282
305
  });
283
306
  }),
284
- { businessBirthInternal: true as const },
307
+ { businessBirthInternal: true as const, ownedAuthoringInternal: true as const },
285
308
  );
286
309
 
287
310
  function recordScope(aggregate: AggregateHandle, recordId: string): string {
@@ -295,7 +318,10 @@ function recordObject(aggregate: AggregateHandle, recordId: string): string {
295
318
  function ownedRecordActionDirectives(aggregate: AggregateHandle) {
296
319
  const share = Object.assign(
297
320
  directive(ownedRecordActionDirectiveId("share", aggregate.id))
298
- .ensures(WrappedKey)
321
+ // The owned business record is the semantic target. Relation tuples and wraps are framework
322
+ // effects of sharing it; making a wrap the primary target causes authenticated law to describe
323
+ // this as a key action and the kernel correctly refuses `share(record, person)`.
324
+ .mutates(aggregate)
299
325
  .payload(z.object({
300
326
  recordId: z.string().min(1),
301
327
  peer: principal,
@@ -313,13 +339,14 @@ function ownedRecordActionDirectives(aggregate: AggregateHandle) {
313
339
  }),
314
340
  ...p.wraps.flatMap((entry) => writeWrap(recordScope(aggregate, p.recordId), entry.epoch, entry)),
315
341
  ])
316
- .alsoEnsures(RelationTuple),
317
- { businessBirthInternal: true as const },
342
+ .alsoEnsures(RelationTuple)
343
+ .alsoEnsuresMany(WrappedKey),
344
+ { businessBirthInternal: true as const, ownedAuthoringInternal: true as const },
318
345
  );
319
346
 
320
347
  const unshare = Object.assign(
321
348
  directive(ownedRecordActionDirectiveId("unshare", aggregate.id))
322
- .ensures(KeyScope)
349
+ .mutates(aggregate)
323
350
  .payload(z.object({
324
351
  recordId: z.string().min(1),
325
352
  peer: principal,
@@ -340,14 +367,15 @@ function ownedRecordActionDirectives(aggregate: AggregateHandle) {
340
367
  ...writeKeyEpoch(recordScope(aggregate, p.recordId), p.epoch, p.changedAt, p.wraps),
341
368
  ])
342
369
  .alsoEnsures(RelationTuple)
370
+ .alsoEnsures(KeyScope)
343
371
  .alsoEnsuresMany(WrappedKey),
344
- { businessBirthInternal: true as const },
372
+ { businessBirthInternal: true as const, ownedAuthoringInternal: true as const },
345
373
  );
346
374
 
347
375
  const ownerField = aggregate.audience?.kind === "owned" ? aggregate.audience.ownerField : "";
348
376
  const transfer = Object.assign(
349
377
  directive(ownedRecordActionDirectiveId("transfer", aggregate.id))
350
- .ensures(KeyScope)
378
+ .mutates(aggregate)
351
379
  .payload(z.object({
352
380
  recordId: z.string().min(1),
353
381
  newOwner: principal,
@@ -383,9 +411,9 @@ function ownedRecordActionDirectives(aggregate: AggregateHandle) {
383
411
  return ops;
384
412
  })
385
413
  .alsoEnsuresMany(RelationTuple)
386
- .alsoMutates(aggregate)
414
+ .alsoEnsures(KeyScope)
387
415
  .alsoEnsuresMany(WrappedKey),
388
- { businessBirthInternal: true as const },
416
+ { businessBirthInternal: true as const, ownedAuthoringInternal: true as const },
389
417
  );
390
418
 
391
419
  return { share, unshare, transfer };
@@ -393,8 +421,11 @@ function ownedRecordActionDirectives(aggregate: AggregateHandle) {
393
421
 
394
422
  const APPROVAL_EXPORT = "__nomos_owned_device_approval_7c1f5e";
395
423
  const IDENTITY_EXPORT = "__nomos_owned_identity_relations_7c1f5e";
424
+ const CUSTODY_RELATIONS_EXPORT = "__nomos_owned_custody_relations_7c1f5e";
396
425
  const WARRANT_EXPORT = "__nomos_owned_genesis_warrant_7c1f5e";
397
426
  const SEED_EXPORT = "__nomos_owned_seed_genesis_7c1f5e";
427
+ const BIRTH_ATTESTATION_EXPORT = "__nomos_owned_birth_attestation_7c1f5e";
428
+ const BIRTH_ATTESTATION_SEED_EXPORT = "__nomos_owned_seed_birth_attestation_7c1f5e";
398
429
  const ENROLL_EXPORT = "__nomos_owned_enroll_signer_7c1f5e";
399
430
  const INITIALIZE_EXPORT = "__nomos_owned_initialize_identity_7c1f5e";
400
431
  const KEY_SCOPE_EXPORT = "__nomos_owned_key_scope_7c1f5e";
@@ -411,11 +442,15 @@ const RECOVERY_FAIL_EXPORT = "__nomos_owned_recovery_fail_7c1f5e";
411
442
  const RECOVERY_BLOCK_EXPORT = "__nomos_owned_recovery_block_7c1f5e";
412
443
  const RECOVERY_DEAD_LETTER_EXPORT = "__nomos_owned_recovery_dead_letter_7c1f5e";
413
444
  const RECOVERY_QUERY_EXPORT = "__nomos_owned_recovery_query_7c1f5e";
445
+ const RECOVERY_CAPABILITY_EXPORT = "__nomos_owned_recovery_capability_7c1f5e";
414
446
 
415
447
  const ownedGenesisWarrant = hidden(GenesisWarrant);
416
448
  const ownedSeedGenesis = hidden(seedGenesis);
449
+ const ownedBirthAttestation = hidden(BirthAttestation);
450
+ const ownedSeedBirthAttestation = hidden(seedBirthAttestation);
417
451
  const ownedEnrollSigner = hidden(enrollSigner);
418
452
  const ownedIdentityRelations = hidden(identityRelations);
453
+ const ownedCustodyRelationLaw = hidden(ownedCustodyRelations);
419
454
 
420
455
  /**
421
456
  * The published CLI bundles its compiler while a tenant module imports declarations from the separately
@@ -476,6 +511,7 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
476
511
  [RECOVERY_BLOCK_EXPORT, hiddenRecoveryLifecycle.block],
477
512
  [RECOVERY_DEAD_LETTER_EXPORT, hiddenRecoveryLifecycle.deadLetter],
478
513
  [RECOVERY_QUERY_EXPORT, hidden(ownedRecoveryOrdersByRecoveryPrincipal)],
514
+ [RECOVERY_CAPABILITY_EXPORT, hidden(ownedRecoveryCapability)],
479
515
  ] as const;
480
516
 
481
517
  // These names are deliberately obscure, not magical. A tenant can still reach one accidentally or
@@ -489,6 +525,10 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
489
525
  && !sameFrameworkDeclaration(mod[IDENTITY_EXPORT], ownedIdentityRelations)) {
490
526
  reservedCollision(IDENTITY_EXPORT);
491
527
  }
528
+ if (Object.prototype.hasOwnProperty.call(mod, CUSTODY_RELATIONS_EXPORT)
529
+ && !sameFrameworkDeclaration(mod[CUSTODY_RELATIONS_EXPORT], ownedCustodyRelationLaw)) {
530
+ reservedCollision(CUSTODY_RELATIONS_EXPORT);
531
+ }
492
532
  for (const [key, generated] of recoveryInjections) {
493
533
  if (Object.prototype.hasOwnProperty.call(mod, key) && !sameFrameworkDeclaration(mod[key], generated)) {
494
534
  reservedCollision(key);
@@ -497,6 +537,8 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
497
537
  for (const [key, generated] of [
498
538
  [WARRANT_EXPORT, ownedGenesisWarrant],
499
539
  [SEED_EXPORT, ownedSeedGenesis],
540
+ [BIRTH_ATTESTATION_EXPORT, ownedBirthAttestation],
541
+ [BIRTH_ATTESTATION_SEED_EXPORT, ownedSeedBirthAttestation],
500
542
  [ENROLL_EXPORT, ownedEnrollSigner],
501
543
  ] as const) {
502
544
  if (Object.prototype.hasOwnProperty.call(mod, key) && !sameFrameworkDeclaration(mod[key], generated)) {
@@ -526,6 +568,8 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
526
568
  for (const [framework, generated] of [
527
569
  [GenesisWarrant, ownedGenesisWarrant],
528
570
  [seedGenesis, ownedSeedGenesis],
571
+ [BirthAttestation, ownedBirthAttestation],
572
+ [seedBirthAttestation, ownedSeedBirthAttestation],
529
573
  [enrollSigner, ownedEnrollSigner],
530
574
  ] as const) {
531
575
  if (
@@ -546,6 +590,14 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
546
590
  ) {
547
591
  reservedCollision(`global relation type '${identityRelations.type}'`);
548
592
  }
593
+ if (
594
+ !sameFrameworkDeclaration(value, ownedCustodyRelationLaw)
595
+ && isRelationTypeDecl(value)
596
+ && value.global === true
597
+ && value.type === ownedCustodyRelations.type
598
+ ) {
599
+ reservedCollision(`global relation type '${ownedCustodyRelations.type}'`);
600
+ }
549
601
  if (value !== null && typeof value === "object" && typeof (value as { id?: unknown }).id === "string") {
550
602
  const id = (value as { id: string }).id;
551
603
  const generated = actions.flatMap((entry) => [entry.share, entry.unshare, entry.transfer]);
@@ -560,6 +612,7 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
560
612
  [APPROVAL_EXPORT]: approveOwnedDevice,
561
613
  [INITIALIZE_EXPORT]: initializeOwnedIdentity,
562
614
  [IDENTITY_EXPORT]: ownedIdentityRelations,
615
+ [CUSTODY_RELATIONS_EXPORT]: ownedCustodyRelationLaw,
563
616
  [KEY_SCOPE_EXPORT]: hidden(KeyScope),
564
617
  [WRAPPED_KEY_EXPORT]: hidden(WrappedKey),
565
618
  [KEY_SCOPE_QUERY_EXPORT]: hidden(keyScopeByScope),
@@ -574,11 +627,14 @@ export function withOwnedCustodyExports<M extends Record<string, unknown>>(mod:
574
627
  [RECOVERY_BLOCK_EXPORT]: hiddenRecoveryLifecycle.block,
575
628
  [RECOVERY_DEAD_LETTER_EXPORT]: hiddenRecoveryLifecycle.deadLetter,
576
629
  [RECOVERY_QUERY_EXPORT]: hidden(ownedRecoveryOrdersByRecoveryPrincipal),
630
+ [RECOVERY_CAPABILITY_EXPORT]: hidden(ownedRecoveryCapability),
577
631
  // A local law test starts from an empty root, while production business state is normally born into a
578
632
  // warranted child. Keep the same lawful bootstrap machinery available in both cases, but framework-only:
579
633
  // the harness can seat its test identity and warrant the root without adding a tenant-facing lane.
580
634
  [WARRANT_EXPORT]: ownedGenesisWarrant,
581
635
  [SEED_EXPORT]: ownedSeedGenesis,
636
+ [BIRTH_ATTESTATION_EXPORT]: ownedBirthAttestation,
637
+ [BIRTH_ATTESTATION_SEED_EXPORT]: ownedSeedBirthAttestation,
582
638
  [ENROLL_EXPORT]: ownedEnrollSigner,
583
639
  };
584
640
  for (const { aggregate, share, unshare, transfer } of actions) {
@@ -104,7 +104,7 @@ export { enrollFirstDevice, revokeSigner } from "./signing_identity.js";
104
104
  // (this trio) but DELIBERATELY does NOT export the `parentAttestedBirth` era marker — a platform's own 1-link
105
105
  // `[platform_cert]` chain would fail `verify_to_root` (>=2 links), so its own verify doors must stay inert
106
106
  // while it still carries the cert for its children. Fully additive (new aggregate + directive, no era key ⇒
107
- // pre-existing chains replay byte-identical). architecture/co2_attestation_spine.md.
107
+ // pre-existing chains replay byte-identical). See architecture/verified_auth_from_genesis.md.
108
108
  export { BIRTH_ATTESTATION_ID, BirthAttestation, birthAttestation, seedBirthAttestation } from "./home.js";
109
109
  // THE ENROLL-ONCE DELEGATION LANE — a platform records each tenant's device-key delegation (signed by its
110
110
  // onboarding signer S_P) so the kernel reads it at `signer-delegation:<user>` when assembling a home's chain.
package/src/index.ts CHANGED
@@ -464,7 +464,7 @@ export {
464
464
  // a `requires(cap)` aggregate's cap IS a read relation (synthesized into nomosRelations at compile),
465
465
  // and a grant is a `relateSubject({ object:"workspace:self", relation:cap, ... })` tuple. See
466
466
  // framework/relations.ts + relation_schema.ts below.
467
- // NATIVE ZANZIBAR / ReBAC (architecture/zanzibar_native_plan.md, slice 1) — object-keyed
467
+ // NATIVE ZANZIBAR / ReBAC (architecture/authz_relationship_spine.md) — object-keyed
468
468
  // relation-tuple events `object#relation@subject`, the clean-slate-native authz spine.
469
469
  export {
470
470
  RELATION_STATUSES,
package/src/law_check.ts CHANGED
@@ -1351,7 +1351,7 @@ export function checkDomainModules(
1351
1351
  domain,
1352
1352
  level: "warn",
1353
1353
  what: `birth recipe: ${diag}`,
1354
- fix: `see architecture/g3_kernel_fulfils_birth.md / the births DX — the recipe must install both framework + child law and seed genesis`,
1354
+ fix: `see create-githolon/template/docs/08-births.md — the recipe must install both framework + child law and seed genesis`,
1355
1355
  });
1356
1356
  }
1357
1357
  }
package/src/law_graph.ts CHANGED
@@ -1221,6 +1221,7 @@ export function discoverLawGraph(
1221
1221
  const frameworkCapabilities = new Set<"owned-authoring">();
1222
1222
  if (
1223
1223
  businessBirth?.owned === true ||
1224
+ (intent as { ownedAuthoringInternal?: unknown }).ownedAuthoringInternal === true ||
1224
1225
  affectedAggregateIds.some((aggregateId) =>
1225
1226
  aggregateByLabel.get(aggregateId)?.node.ownerField !== undefined)
1226
1227
  ) {
@@ -1537,16 +1538,23 @@ export function discoverLawGraph(
1537
1538
  const synthesizedReadRelations = new Map<string, (typeof authoredRelationDecls)[number]>();
1538
1539
  for (const aggregate of module.aggregates) {
1539
1540
  if (aggregate.audience === undefined || aggregate.audience.kind === "public") continue;
1540
- const relation = aggregate.audience.kind === "owned"
1541
- ? OWNED_READ_RELATION
1542
- : aggregate.audience.cap;
1543
- const label = `workspace#${relation}`;
1541
+ const owned = aggregate.audience.kind === "owned";
1542
+ const relation = owned ? OWNED_READ_RELATION : aggregate.audience.cap;
1543
+ // A normal capability is a workspace role. `.owned()` is deliberately different: Nomos seats the
1544
+ // creator on each record's own relation so sharing one record grants no sibling. The old synthesis
1545
+ // declared the owned relation on `workspace:self` while the framework correctly wrote
1546
+ // `<domain>.<Aggregate>:<id>#reader`; every lawful owner read was consequently denied.
1547
+ const authoredObjectType = owned ? aggregate.id : "workspace";
1548
+ const objectType = owned
1549
+ ? aggregateByLabel.get(aggregate.id)!.node.wireType
1550
+ : "workspace";
1551
+ const label = `${authoredObjectType}#${relation}`;
1544
1552
  if (authoredRelationLabels.has(label) || synthesizedReadRelations.has(label)) continue;
1545
1553
  synthesizedReadRelations.set(label, {
1546
1554
  label,
1547
- authoredObjectType: "workspace",
1548
- objectType: "workspace",
1549
- global: true,
1555
+ authoredObjectType,
1556
+ objectType,
1557
+ global: !owned,
1550
1558
  caps: DEFAULT_RELATION_CAPS,
1551
1559
  relation,
1552
1560
  definition: {
@@ -1697,16 +1705,17 @@ export function discoverLawGraph(
1697
1705
  }
1698
1706
  for (const aggregate of module.aggregates) {
1699
1707
  if (aggregate.audience === undefined || aggregate.audience.kind === "public") continue;
1700
- const relationName = aggregate.audience.kind === "owned"
1701
- ? OWNED_READ_RELATION
1702
- : aggregate.audience.cap;
1708
+ const owned = aggregate.audience.kind === "owned";
1709
+ const relationName = owned ? OWNED_READ_RELATION : aggregate.audience.cap;
1710
+ const objectType = owned ? aggregate.id : "workspace";
1711
+ const relationLabel = `${objectType}#${relationName}`;
1703
1712
  const candidates = relationNodes.filter((candidate) =>
1704
- candidate.entity.label === `workspace#${relationName}`);
1713
+ candidate.entity.label === relationLabel);
1705
1714
  if (candidates.length > 1) {
1706
1715
  fail(`aggregate '${aggregate.id}' read relation '${relationName}' is ambiguous across relation types`);
1707
1716
  }
1708
1717
  const readPolicy: LawTargetRef<"relation" | "policy"> = candidates[0] === undefined
1709
- ? { kind: "dependency-export", entityKind: "relation", label: `workspace#${relationName}` }
1718
+ ? { kind: "dependency-export", entityKind: "relation", label: relationLabel }
1710
1719
  : ref(candidates[0].entity);
1711
1720
  const entry = aggregateByLabel.get(aggregate.id)!;
1712
1721
  const updated = { ...entry.node, readPolicy };
@@ -6,7 +6,7 @@
6
6
  /**
7
7
  * relation_schema — THE NATIVE ZANZIBAR RELATION SCHEMA (the userset-rewrite law).
8
8
  *
9
- * This is layer 2 of the three-layer authz design (see `architecture/zanzibar_native_plan.md`):
9
+ * This is the schema layer of the authorization design (see `architecture/authz_relationship_spine.md`):
10
10
  * 1. TUPLES = ledger events (`relateSubject`/`RelationTuple`, framework/relations.ts).
11
11
  * 2. SCHEMA = THIS FILE — per-type relation REWRITES authored as PURE DATA, lowered to the
12
12
  * `nomosRelations` USD era key. NO JavaScript on the schema path: a `.relation(...)` produces
@@ -27,7 +27,7 @@
27
27
  * directly(user(), key()) → { rewrite: this, allow:[...] } — the this+allow PAIR so the
28
28
  * type restriction CAN'T be forgotten (the #1 footgun).
29
29
  *
30
- * 2. SECURE-BY-DEFAULT AUDIENCES (the PRIMARY surface; `zanzibar_native_plan.md` F2). Builder
30
+ * 2. SECURE-BY-DEFAULT AUDIENCES (the PRIMARY surface; `authz_relationship_spine.md`). Builder
31
31
  * methods that lower to a relation (or a small set of relations) with the SAFE default baked in:
32
32
  * .public(rel) → rel = directly(anyUser()) (user:* ONLY — never key:*)
33
33
  * .owned(rel?) → owner = directly(user()); rel = directly(self) ∪ computed(owner)
@@ -138,7 +138,7 @@ function payloadFieldKind(spec: PayloadFieldSpec): string {
138
138
  return "string";
139
139
  case "vo":
140
140
  case "voList":
141
- // Interface kinds stay at the base lane for now (slice D of vo_authoring_end_to_end.md
141
+ // Interface kinds stay at the base lane for now (the remaining parity work in dart_codegen_parity.md
142
142
  // upgrades these to a first-class "vo" kind) — a VO IS a json leaf structurally.
143
143
  return "json";
144
144
  case "scalar":
@@ -608,7 +608,7 @@ function describeZod(
608
608
  const VO_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
609
609
 
610
610
  /**
611
- * THE VO BRAND (slice A of vo_authoring_end_to_end.md) — `valueObject()` stamps its zod schema
611
+ * THE VO BRAND (vo_first_class.md) — `valueObject()` stamps its zod schema
612
612
  * with the VO's {@link ValueObjectFieldMeta} under this symbol. The property itself is
613
613
  * non-enumerable, never serialized and never read by the engine's validation path. When attached
614
614
  * to an aggregate field, its canonical descriptor deliberately enters that field's stable value contract.
@@ -1036,7 +1036,7 @@ export const geo = {
1036
1036
  /**
1037
1037
  * Collect the value objects referenced by DIRECTIVE PAYLOADS (top-level fields + array
1038
1038
  * elements), via the {@link NOMOS_VO_BRAND} the schemas carry. CODEGEN-ONLY (slice B of
1039
- * vo_authoring_end_to_end.md): the result feeds the generated Dart/TS surfaces so a
1039
+ * vo_first_class.md): the result feeds the generated Dart/TS surfaces so a
1040
1040
  * payload-only VO still gets its class/interface — it does NOT enter the `nomosValueObjects`
1041
1041
  * IR key (that stays aggregate-derived, keeping package bytes / domainHash untouched).
1042
1042
  */