@githolon/dsl 0.105.6 → 0.105.7

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();
@@ -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.7",
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();
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);
package/src/codegen_ts.ts CHANGED
@@ -2090,6 +2090,25 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2090
2090
  ` return read(keys);`,
2091
2091
  `};`,
2092
2092
  ``,
2093
+ `/** Owned bounded reads perform the same record-key-aware second pass without erasing`,
2094
+ ` * the page envelope. Keeping this distinct from the array helper makes cursor semantics explicit. */`,
2095
+ `const _readPageWithOwnedKeys = async <T>(`,
2096
+ ` holon: NomosHolon,`,
2097
+ ` keyStore: NomosKeyStore | undefined,`,
2098
+ ` scopes: readonly { scope: string; owned?: true }[],`,
2099
+ ` read: (keys: readonly NomosScopeKey[] | undefined) => Promise<{ rows: T[]; cursor: string | null; done: boolean }>,`,
2100
+ `): Promise<{ rows: T[]; cursor: string | null; done: boolean }> => {`,
2101
+ ` const firstKeys = await _readKeys(holon, keyStore, scopes);`,
2102
+ ` const first = await read(firstKeys);`,
2103
+ ` if (!scopes.some((scope) => scope.owned === true) || first.rows.length === 0) return first;`,
2104
+ ` const records = first.rows.map((row) => {`,
2105
+ ` const raw = row as T & { type: string; id: string };`,
2106
+ ` return { type: raw.type, id: raw.id };`,
2107
+ ` });`,
2108
+ ` const keys = await _readKeys(holon, keyStore, scopes, records);`,
2109
+ ` return read(keys);`,
2110
+ `};`,
2111
+ ``,
2093
2112
  `/** Reactive owned reads use the exact same two-pass, record-key-aware read as one-shot reads.`,
2094
2113
  ` * The raw watch is only a local change signal; ciphertext rows never reach application callbacks. */`,
2095
2114
  `const _watchWithOwnedKeys = <T>(`,
@@ -3375,7 +3394,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
3375
3394
  );
3376
3395
  const queryPageCall = qScopes.length
3377
3396
  ? 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 }>)`
3397
+ ? `_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
3398
  : `(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
3399
  : `(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
3400
  methods.push(
@@ -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.45";
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).
@@ -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) {
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 };