@githolon/dsl 0.80.2 → 0.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -31,6 +31,7 @@ library;
31
31
  export 'package:nomos_types/nomos_types.dart';
32
32
  // homeNameOf(subject) — the canonical home-workspace name (parity with @githolon/client + the law side).
33
33
  export 'src/home.dart';
34
+ export 'src/workspace_name.dart';
34
35
  // THE WEBVIEW BRIDGE (app-binding boundary) — the async Dart runtime that drives the REAL githolon running
35
36
  // in a WebView, so the generated typed client never needs a `MemoryPersistence` fake.
36
37
  export 'src/webview_bridge.dart';
@@ -1827,6 +1827,33 @@ class NomosBridge {
1827
1827
  Map<String, Object?> args) async =>
1828
1828
  _asMap(await _call('attestationSign', {'args': args}));
1829
1829
 
1830
+ /// Issue a ROLE-DELEGATION cert (the `.delegatedRole()` lane, kernel `delegationCertSign` op). A platform
1831
+ /// signs — with its grantor secret `signerSecret` (base64) — a cert granting `relation` to `subject`,
1832
+ /// carrying the lineage `certChain` up to K_root. Returns `{ certString }`; deliver it into the target
1833
+ /// home via the tenant's `acceptDelegatedRole` directive (the one gate re-verifies it to K_root on every
1834
+ /// lane). OURS = the op; operating the grantor secret + choosing who to grant is the TENANT.
1835
+ /// final res = await bridge.issueDelegationCert(signerSecret: sP, grantorId: 'co2-platform',
1836
+ /// relation: 'co2EstateCreator', subject: userSubject, epoch: 1, certChain: [rootLink]);
1837
+ /// final certString = res['certString'] as String;
1838
+ Future<Map<String, Object?>> issueDelegationCert({
1839
+ required String signerSecret,
1840
+ required String grantorId,
1841
+ required String relation,
1842
+ required String subject,
1843
+ int epoch = 1,
1844
+ List<Object?>? certChain,
1845
+ }) async =>
1846
+ _asMap(await _call('issueDelegationCert', {
1847
+ 'args': {
1848
+ 'signerSecret': signerSecret,
1849
+ 'grantorId': grantorId,
1850
+ 'relation': relation,
1851
+ 'subject': subject,
1852
+ 'epoch': epoch,
1853
+ if (certChain != null) 'certChain': certChain,
1854
+ },
1855
+ }));
1856
+
1830
1857
  /// A declared query's rows from the local projection. `keys` (E2E sealing) injects the reader's held
1831
1858
  /// scope keys so encrypted fields AEAD-open in-wasm; absent ⇒ they stay opaque envelopes (today's
1832
1859
  /// behavior). `principal` stamps the private-read gate (defaults to the connection's bound principal).
@@ -0,0 +1,24 @@
1
+ import 'dart:convert';
2
+
3
+ import 'package:crypto/crypto.dart';
4
+
5
+ /// The unit separator (0x1F) that disambiguates `(namespace, key)` in the name hash input.
6
+ final String workspaceNameSep = String.fromCharCode(31);
7
+
8
+ /// Re-derive the workspace name a keyed birth produced, so you can ADDRESS a born workspace from a key you
9
+ /// own — with zero persistence, on any device, any time.
10
+ ///
11
+ /// A workspace exists to be used, and using it means finding it again. When a law births a child with
12
+ /// `birth.keyed({ key, namespace })` (the `@githolon/dsl` verb), the workspace name is
13
+ /// `<namespace>-<sha256(namespace + SEP + key)[:40]>`. This function computes the IDENTICAL name — it is the
14
+ /// byte-for-byte mirror of `@githolon/dsl`'s `workspaceNameForKey` and `@githolon/client`'s
15
+ /// `deriveWorkspaceName`, contract-tested against shared vectors. So instead of persisting a minted name or
16
+ /// hand-rolling an `estateWorkspaceForEstateId` helper, you write:
17
+ ///
18
+ /// ```dart
19
+ /// final ws = deriveWorkspaceName('estate', estateId); // open / sync / mount it
20
+ /// ```
21
+ String deriveWorkspaceName(String namespace, String key) {
22
+ final digest = sha256.convert(utf8.encode('$namespace$workspaceNameSep$key')).toString();
23
+ return '$namespace-${digest.substring(0, 40)}';
24
+ }
@@ -0,0 +1,24 @@
1
+ import 'package:nomos_client/src/workspace_name.dart';
2
+ import 'package:test/test.dart';
3
+
4
+ // CONTRACT VECTORS — shared byte-for-byte with @githolon/dsl `workspaceNameForKey` and @githolon/client
5
+ // `deriveWorkspaceName`. Generated with real sha256 (node crypto). If any of the three derivations drifts,
6
+ // a workspace born by one peer becomes unaddressable by another — so these must never change silently.
7
+ void main() {
8
+ group('deriveWorkspaceName — the shared birth-name derivation', () {
9
+ test('matches the cross-language reference vectors', () {
10
+ expect(deriveWorkspaceName('estate', 'estate-abc123'),
11
+ 'estate-83662e729ba18cd04bde52c7a8d854594833d70e');
12
+ expect(deriveWorkspaceName('home', 'user:ada'),
13
+ 'home-561af44c94b5b80824f1a9e8cfa1095105f3cb34');
14
+ expect(deriveWorkspaceName('catalogue', 'cat-42'),
15
+ 'catalogue-f50bf3b9732dc36961b05b8e6503d96df5f3e88e');
16
+ });
17
+
18
+ test('is deterministic and namespaced (no cross-namespace collision)', () {
19
+ expect(deriveWorkspaceName('estate', 'z'), deriveWorkspaceName('estate', 'z'));
20
+ expect(deriveWorkspaceName('estate', 'z'), isNot(deriveWorkspaceName('catalogue', 'z')));
21
+ expect(deriveWorkspaceName('estate', 'z').startsWith('estate-'), isTrue);
22
+ });
23
+ });
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.80.2",
3
+ "version": "0.82.0",
4
4
  "type": "module",
5
5
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
package/src/births.ts CHANGED
@@ -79,6 +79,133 @@ export function birth(recipe: BirthRecipe): void {
79
79
  footprint.push(recipe);
80
80
  }
81
81
 
82
+ // `nomos.sha256` — the trustless pure hash bound in the sealed engine (like `nomos.mint`), used to derive a
83
+ // keyed child's DETERMINISTIC, ADDRESSABLE name in-law so every peer — and every CLIENT — re-derives the
84
+ // same name from the same key.
85
+ declare const nomos: { sha256(input: string): string };
86
+
87
+ /**
88
+ * THE ONE WORKSPACE-NAME DERIVATION — shared verbatim by the law (this engine), the Dart client, and the TS
89
+ * client, so a workspace born under a key is ADDRESSABLE from that key on every peer with zero persistence.
90
+ *
91
+ * name = `<namespace>-<sha256(namespace + "" + key)[:40]>`
92
+ *
93
+ * A workspace exists to be USED, and using it means finding it again — so a birth's name is NEVER opaque or
94
+ * minted-and-forgotten; it is a pure function of a key the caller already owns (a business id, an owner uid).
95
+ * The `` (unit separator) makes `(ns,key)` unambiguous. `nomos.sha256` returns lowercase hex.
96
+ *
97
+ * The client mirror is `deriveWorkspaceName(namespace, key)` in `nomos_client` (Dart) and `@githolon/client`
98
+ * (TS) — byte-identical output is contract-tested against {@link WORKSPACE_NAME_VECTORS}.
99
+ */
100
+ export const WORKSPACE_NAME_SEP = "\u001f"; // unit separator disambiguates (namespace,key)
101
+ export function workspaceNameForKey(namespace: string, key: string): string {
102
+ return namespace + "-" + nomos.sha256(String(namespace) + WORKSPACE_NAME_SEP + String(key)).slice(0, 40);
103
+ }
104
+
105
+ /** A genesis chain, or a builder that receives the resolved child workspace name (so a seed step can embed
106
+ * it — e.g. `seedEstate` needs to write its own `estateName`). Passing a builder is the common case. */
107
+ // An array of genesis steps, OR a builder that receives the resolved child name. Typed as `unknown[]`
108
+ // (not `unknown`) so the union doesn't collapse — the builder's `childWorkspace` param infers as `string`.
109
+ export type GenesisChainOrBuilder = readonly unknown[] | ((childWorkspace: string) => readonly unknown[]);
110
+ function resolveChain(child: string, c: GenesisChainOrBuilder): unknown {
111
+ return typeof c === "function" ? c(child) : c;
112
+ }
113
+
114
+ /** A `create()`d aggregate reference — the birth's audit record, whose MINTED (captured, replay-stable) id
115
+ * becomes the instance child's workspace name. Only its `__id` is read. */
116
+ interface MintedRef {
117
+ readonly __id: string;
118
+ }
119
+
120
+ /**
121
+ * `birth.keyed(...)` — THE way to birth a child workspace. A workspace exists to be USED, and using it means
122
+ * FINDING IT AGAIN, so its name is ALWAYS derived from a `key` the caller owns (a business id, an owner uid)
123
+ * via {@link workspaceNameForKey}. The client re-derives the SAME name with `deriveWorkspaceName(namespace,
124
+ * key)` — so the born workspace is addressable from the key alone, with zero persistence. Same key ⇒ same
125
+ * workspace (re-birth is idempotent). This is the only birth verb you need.
126
+ *
127
+ * ```ts
128
+ * export const birthEstate = directive("birthEstate")
129
+ * .creates(EstateBirth)
130
+ * .payload(z.object({ estateId: z.string(), owner: z.string(), frameworkHash: z.string(), lawHash: z.string(), bornAt: z.string() }))
131
+ * .plan((p) => {
132
+ * birth.keyed({ key: p.estateId, namespace: "estate", genesisChain: (ws) => [
133
+ * { domain: "bootstrap", directiveId: "installDomain", actor: p.owner, payload: { domainHash: p.frameworkHash }, domainHash: p.frameworkHash, domainPackageB64: "" },
134
+ * { domain: "nomos", directiveId: "installDomain", actor: p.owner, payload: { domainHash: p.lawHash }, domainHash: p.lawHash, domainPackageB64: "" },
135
+ * { domain: "estate", directiveId: "seedEstate", actor: p.owner, payload: { estateId: p.estateId, workspaceName: ws, bornAt: p.bornAt } },
136
+ * ] });
137
+ * return [];
138
+ * })
139
+ * .births().requires("estateCreator");
140
+ * // app (Dart / TS): final ws = client.deriveWorkspaceName('estate', estateId); // open it, any time, any device
141
+ * ```
142
+ */
143
+ export const birthKeyed = ({
144
+ key,
145
+ namespace,
146
+ genesisChain,
147
+ }: {
148
+ /** The id you already own that this workspace is FOR (an estateId, an owner uid). The client addresses the
149
+ * born workspace by re-deriving its name from this same key — so pick a key the client will have in hand. */
150
+ key: string;
151
+ /** The workspace-name namespace (e.g. `"estate"` → `estate-<hash>`). Groups a domain's children. */
152
+ namespace: string;
153
+ genesisChain: GenesisChainOrBuilder;
154
+ }): string => {
155
+ const child = workspaceNameForKey(namespace, String(key));
156
+ footprint.push({ workspace: child, genesisChain: resolveChain(child, genesisChain) });
157
+ return child;
158
+ };
159
+
160
+ /**
161
+ * @deprecated Use {@link birthKeyed}. `birth.instance` mints a name the caller THROWS AWAY — so the born
162
+ * workspace can never be found again except via the birth result. A workspace is a custodial thing you birth
163
+ * in order to use; there is no workspace you make and never address later. Birth by a key you own instead.
164
+ */
165
+ export const birthInstance = ({
166
+ from,
167
+ prefix = "",
168
+ genesisChain,
169
+ }: {
170
+ from: MintedRef;
171
+ prefix?: string;
172
+ genesisChain: GenesisChainOrBuilder;
173
+ }): string => {
174
+ const child = prefix + from.__id;
175
+ footprint.push({ workspace: child, genesisChain: resolveChain(child, genesisChain) });
176
+ return child;
177
+ };
178
+
179
+ /**
180
+ * `birth.singleton(...)` — a keyed birth whose key is a NATURAL single key (one home per user: key = owner).
181
+ * Sugar over {@link birthKeyed}: `birth.singleton({ keyedBy: owner, namespace: "home" })`. Same addressing
182
+ * story — the client re-derives with `deriveWorkspaceName(namespace, keyedBy)`.
183
+ */
184
+ export const birthSingleton = ({
185
+ keyedBy,
186
+ namespace,
187
+ genesisChain,
188
+ }: {
189
+ /** The natural single key (e.g. the owner). One workspace per key; re-birth idempotent. */
190
+ keyedBy: string | readonly string[];
191
+ /** The workspace-name namespace (e.g. `"home"`). */
192
+ namespace: string;
193
+ genesisChain: GenesisChainOrBuilder;
194
+ }): string => {
195
+ const key = (Array.isArray(keyedBy) ? keyedBy : [keyedBy]).map(String).join(WORKSPACE_NAME_SEP);
196
+ return birthKeyed({ key, namespace, genesisChain });
197
+ };
198
+
199
+ // Function/namespace declaration merging: attach the birth verbs to `birth` so the call site reads
200
+ // `birth.keyed(...)` — with full types AND the runtime assignment in one place.
201
+ // eslint-disable-next-line @typescript-eslint/no-namespace
202
+ export namespace birth {
203
+ export const keyed = birthKeyed;
204
+ export const singleton = birthSingleton;
205
+ /** @deprecated Use `birth.keyed` — see {@link birthInstance}. */
206
+ export const instance = birthInstance;
207
+ }
208
+
82
209
  /**
83
210
  * THE PEER-SOVEREIGN ROOT-OF-TRUST SENTINEL (the BirthAttestation cert chain). A home/child law EXPORTS
84
211
  * this marker to declare it REQUIRES a parent-attested-birth: a warranted workspace running this law must
package/src/directive.ts CHANGED
@@ -169,6 +169,28 @@ export interface Directive<P = unknown> {
169
169
  readonly signature: string;
170
170
  };
171
171
  };
172
+ /**
173
+ * DELEGATED-AUTHORITY DELIVERY (`delegatedRole`/`.delegatedFrom(parent)`,
174
+ * architecture/delegated_authority_births.md). When set, this directive folds a DELEGATED relation tuple
175
+ * ONLY if its payload carries a valid `DelegationCert` — a platform P's SIGNED grant, rooting at `K_root`,
176
+ * binding the SAME (relation, subject) the tuple writes, and (`.delegatedFrom(parent)`) whose grantor key
177
+ * is THIS workspace's declared parent key. Lowered into the `nomosDelegationGate` era key so the one gate
178
+ * (author / admission / verify_chain) re-verifies the grant OFFLINE — the delivered fact H's own
179
+ * `.requires(relation)` birth gate then reads locally. The kernel hardcodes no id/field — all law-declared.
180
+ */
181
+ readonly delegatedRoleGate?: {
182
+ /** The delegated relation the folded tuple writes (`co2EstateCreator`) — the cert must grant exactly it. */
183
+ readonly relation: string;
184
+ /** The payload field carrying the delegated subject (a `user:`/`key:` string). */
185
+ readonly subjectField: string;
186
+ /** The payload field carrying the `DelegationCert` (JSON string). */
187
+ readonly certField: string;
188
+ /** `.delegatedFrom(parent)`: the singleton INSTANCE id whose field holds this workspace's parent key
189
+ * (e.g. `birth-attestation:self` / a seeded `platform-key:self`). "" ⇒ from-parent binding inert. */
190
+ readonly parentKeyObject: string;
191
+ /** The field on that singleton holding the parent's base64 ed25519 public key (e.g. `parentKey`). */
192
+ readonly parentKeyField: string;
193
+ };
172
194
  /**
173
195
  * THE KEYLESS VERIFIED-FIRST-DEVICE GATE (architecture/keyless_verified_onboard.md). When set, this
174
196
  * directive bootstraps a principal's FIRST device-signer from an ON-CHAIN, device-bound IdP attestation:
@@ -365,6 +387,21 @@ export interface RequirableDirective<P> extends Directive<P> {
365
387
  * host-free. Framework-only (the home law's `recordShare`). Returns a NEW requirable directive.
366
388
  */
367
389
  attestationAuthorized(owner: { ownerObject: string; ownerField: string }): RequirableDirective<P>;
390
+ /**
391
+ * Declare this directive a DELEGATED-AUTHORITY delivery (`delegated_authority_births.md`): its write folds
392
+ * the delegated `relation` tuple ONLY behind a carried `DelegationCert` P signed — lowered into the
393
+ * `nomosDelegationGate` era key so the one gate re-verifies the grant to `K_root` OFFLINE on every peer.
394
+ * `subjectField`/`certField` default to `"subject"`/`"delegationCert"`. Chain `.delegatedFrom(parent)` to
395
+ * bind the grantor to this workspace's parent key. Returns a NEW requirable directive.
396
+ */
397
+ delegatedRole(relation: string, opts?: { subjectField?: string; certField?: string }): RequirableDirective<P>;
398
+ /**
399
+ * `.delegatedFrom(parent)` — bind the delegating grantor to THIS workspace's parent key (the platform P
400
+ * that bore this home): the `DelegationCert`'s grantor key MUST equal the base64 key at
401
+ * `parentKeyObject.parentKeyField` (e.g. the birth-cert `birth-attestation:self`.`parentKey`, or a seeded
402
+ * `platform-key:self`.`grantorKey`). Refines the `.delegatedRole()` gate. Returns a NEW requirable directive.
403
+ */
404
+ delegatedFrom(parent: { parentKeyObject: string; parentKeyField: string }): RequirableDirective<P>;
368
405
  /**
369
406
  * Declare this directive a KEYLESS VERIFIED-FIRST-DEVICE bootstrap (keyless_verified_onboard.md): its
370
407
  * authorization is a carried, device-bound IdP `idpToken` re-verified against the workspace AuthProvider —
@@ -762,6 +799,44 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
762
799
  },
763
800
  });
764
801
  },
802
+ delegatedRole(relation: string, opts?: { subjectField?: string; certField?: string }): RequirableDirective<P> {
803
+ // DELEGATED-AUTHORITY DELIVERY (delegated_authority_births.md): this directive's write folds the
804
+ // delegated `relation` tuple ONLY behind a carried DelegationCert — lowered into the `nomosDelegationGate`
805
+ // era key, so the one gate re-verifies P's grant to K_root OFFLINE on every peer. Field names default to
806
+ // the framework convention here (the kernel reads them from the law's declaration, hardcoding nothing).
807
+ const prior = (base as { delegatedRoleGate?: { parentKeyObject?: string; parentKeyField?: string } })
808
+ .delegatedRoleGate;
809
+ return makeRequirable({
810
+ ...base,
811
+ delegatedRoleGate: {
812
+ relation,
813
+ subjectField: opts?.subjectField ?? "subject",
814
+ certField: opts?.certField ?? "delegationCert",
815
+ parentKeyObject: prior?.parentKeyObject ?? "",
816
+ parentKeyField: prior?.parentKeyField ?? "",
817
+ },
818
+ });
819
+ },
820
+ delegatedFrom(parent: { parentKeyObject: string; parentKeyField: string }): RequirableDirective<P> {
821
+ // .delegatedFrom(parent): bind the grantor to this workspace's parent key (the platform P). Refines the
822
+ // .delegatedRole() gate (a preceding .delegatedRole() supplies relation/fields; this adds the binding).
823
+ const g = (base as { delegatedRoleGate?: {
824
+ relation: string; subjectField: string; certField: string;
825
+ } }).delegatedRoleGate;
826
+ if (g === undefined) {
827
+ throw new Error("directive.delegatedFrom(): call .delegatedRole(relation) before .delegatedFrom(parent)");
828
+ }
829
+ return makeRequirable({
830
+ ...base,
831
+ delegatedRoleGate: {
832
+ relation: g.relation,
833
+ subjectField: g.subjectField,
834
+ certField: g.certField,
835
+ parentKeyObject: parent.parentKeyObject,
836
+ parentKeyField: parent.parentKeyField,
837
+ },
838
+ });
839
+ },
765
840
  verifiedFirstDevice(opts?: {
766
841
  tokenField?: string;
767
842
  keyHashField?: string;
package/src/index.ts CHANGED
@@ -198,7 +198,16 @@ export {
198
198
  // child's genesis recipe via `birth(recipe)` (reuse `buildGenesisChain`); it rides the committed intent as a
199
199
  // declared output, and the kernel — reading the `.births` declaration off the law manifest (the
200
200
  // `nomosBirthGate` era key, never domain state) — spawns + self-validates the child after the offer admits.
201
- export { birth, type BirthRecipe } from "./births.js";
201
+ export {
202
+ birth,
203
+ workspaceNameForKey,
204
+ WORKSPACE_NAME_SEP,
205
+ birthKeyed,
206
+ birthSingleton,
207
+ birthInstance,
208
+ type BirthRecipe,
209
+ type GenesisChainOrBuilder,
210
+ } from "./births.js";
202
211
  // THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain): a home/child law exports
203
212
  // `requireParentAttestedBirth` to declare it requires a parent-attested-birth cert chain (the
204
213
  // `nomosParentAttestedBirth` era key); the kernel verify doors then verify the travelling chain to K_root.
package/src/usd.ts CHANGED
@@ -347,6 +347,25 @@ export interface UsdLayer {
347
347
  }
348
348
  >;
349
349
  };
350
+ /**
351
+ * THE DELEGATED-AUTHORITY GATE (delegated_authority_births.md). Present IFF ≥1 directive declares
352
+ * `.delegatedRole()`: directiveId → {relation, subjectField, certField, parentKeyObject, parentKeyField}.
353
+ * The one gate folds the delegated tuple ONLY behind a DelegationCert P signed (rooting at K_root, binding
354
+ * the same relation+subject, `.delegatedFrom(parent)`). OMITTED for law with no such directive (era rule).
355
+ */
356
+ readonly nomosDelegationGate?: {
357
+ readonly v: 1;
358
+ readonly directives: Record<
359
+ string,
360
+ {
361
+ readonly relation: string;
362
+ readonly subjectField: string;
363
+ readonly certField: string;
364
+ readonly parentKeyObject: string;
365
+ readonly parentKeyField: string;
366
+ }
367
+ >;
368
+ };
350
369
  /**
351
370
  * THE KEYLESS VERIFIED-FIRST-DEVICE GATE (architecture/keyless_verified_onboard.md). Present IFF ≥1
352
371
  * directive declares `.verifiedFirstDevice()`: a per-directive payload FIELD MAPPING. A principal's FIRST
@@ -1136,6 +1155,30 @@ export function emitUsd(
1136
1155
  }
1137
1156
  const nomosAttestationGate =
1138
1157
  Object.keys(attestationReq).length > 0 ? { v: 1 as const, directives: attestationReq } : undefined;
1158
+ // THE DELEGATED-AUTHORITY GATE (delegated_authority_births.md): directives declaring `.delegatedRole()`
1159
+ // → directiveId → its {relation, subjectField, certField, parentKeyObject, parentKeyField}. SORTED keys
1160
+ // (byte-determinism). Omit-when-empty (era rule) — a law with no delegation is byte-identical. The one
1161
+ // gate verifies the carried DelegationCert to K_root + binds (relation, subject) + `.delegatedFrom(parent)`.
1162
+ const delegationReq: Record<
1163
+ string,
1164
+ { relation: string; subjectField: string; certField: string; parentKeyObject: string; parentKeyField: string }
1165
+ > = {};
1166
+ for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
1167
+ const g = (d as { delegatedRoleGate?: {
1168
+ relation: string; subjectField: string; certField: string; parentKeyObject: string; parentKeyField: string;
1169
+ } }).delegatedRoleGate;
1170
+ if (g !== undefined) {
1171
+ delegationReq[d.id] = {
1172
+ relation: g.relation,
1173
+ subjectField: g.subjectField,
1174
+ certField: g.certField,
1175
+ parentKeyObject: g.parentKeyObject,
1176
+ parentKeyField: g.parentKeyField,
1177
+ };
1178
+ }
1179
+ }
1180
+ const nomosDelegationGate =
1181
+ Object.keys(delegationReq).length > 0 ? { v: 1 as const, directives: delegationReq } : undefined;
1139
1182
  // THE ATTESTED-READ GATE (slice B): per-directive `(queryId, from)` edges + the module's
1140
1183
  // trustSource declarations. Emitted iff ≥1 directive declares a foreign read (the era
1141
1184
  // rule). A declared `from` with NO matching trustSource is a NAMED COMPILE ERROR — the
@@ -1492,6 +1535,7 @@ export function emitUsd(
1492
1535
  ...(nomosReadProjectionNamespace !== undefined ? { nomosReadProjectionNamespace } : {}),
1493
1536
  ...(nomosRelationGate !== undefined ? { nomosRelationGate } : {}),
1494
1537
  ...(nomosAttestationGate !== undefined ? { nomosAttestationGate } : {}),
1538
+ ...(nomosDelegationGate !== undefined ? { nomosDelegationGate } : {}),
1495
1539
  ...(nomosAttestedReadGate !== undefined ? { nomosAttestedReadGate } : {}),
1496
1540
  ...(nomosBirthGate !== undefined ? { nomosBirthGate } : {}),
1497
1541
  ...(nomosBirthRecordGate !== undefined ? { nomosBirthRecordGate } : {}),
package/src/usd_layers.ts CHANGED
@@ -198,6 +198,9 @@ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string):
198
198
  ...(layer.nomosAttestationGate !== undefined
199
199
  ? [`${indent}custom string nomos:attestationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosAttestationGate)))}`]
200
200
  : []),
201
+ ...(layer.nomosDelegationGate !== undefined
202
+ ? [`${indent}custom string nomos:delegationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosDelegationGate)))}`]
203
+ : []),
201
204
  ...(layer.nomosAttestedReadGate !== undefined
202
205
  ? [`${indent}custom string nomos:attestedReadGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosAttestedReadGate)))}`]
203
206
  : []),
@@ -875,6 +878,22 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
875
878
  attestationGate = { v: 1, directives: rec };
876
879
  }
877
880
  }
881
+ // THE DELEGATED-AUTHORITY GATE (delegated_authority_births.md): directiveId → delegation spec merged
882
+ // later-wins across layers, SORTED keys. Omit-when-empty (era rule).
883
+ let delegationGate: UsdLayer["nomosDelegationGate"];
884
+ {
885
+ type DelSrc = NonNullable<UsdLayer["nomosDelegationGate"]>["directives"][string];
886
+ const a = new Map<string, DelSrc>();
887
+ for (const l of stack) {
888
+ if (l.nomosDelegationGate === undefined) continue;
889
+ for (const [id, src] of Object.entries(l.nomosDelegationGate.directives)) a.set(id, src);
890
+ }
891
+ if (a.size > 0) {
892
+ const rec: Record<string, DelSrc> = {};
893
+ for (const k of [...a.keys()].sort()) rec[k] = a.get(k)!;
894
+ delegationGate = { v: 1, directives: rec };
895
+ }
896
+ }
878
897
  // THE ATTESTED-READ GATE (slice B): sources + per-directive edges merged later-wins across
879
898
  // layers, SORTED keys (matching the composed emission). Omit-when-empty (era rule).
880
899
  let attestedReadGate: UsdLayer["nomosAttestedReadGate"];
@@ -1031,6 +1050,7 @@ return {
1031
1050
  ...(readProjectionNamespace !== undefined ? { nomosReadProjectionNamespace: readProjectionNamespace } : {}),
1032
1051
  ...(relationGate !== undefined ? { nomosRelationGate: relationGate } : {}),
1033
1052
  ...(attestationGate !== undefined ? { nomosAttestationGate: attestationGate } : {}),
1053
+ ...(delegationGate !== undefined ? { nomosDelegationGate: delegationGate } : {}),
1034
1054
  ...(attestedReadGate !== undefined ? { nomosAttestedReadGate: attestedReadGate } : {}),
1035
1055
  ...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
1036
1056
  ...(birthRecordGate !== undefined ? { nomosBirthRecordGate: birthRecordGate } : {}),
@@ -1173,6 +1193,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
1173
1193
  const relationNamespaceByDomain = new Map<string, UsdLayer["nomosRelationNamespace"]>();
1174
1194
  const readProjectionNamespaceByDomain = new Map<string, UsdLayer["nomosReadProjectionNamespace"]>();
1175
1195
  const attestationGateByDomain = new Map<string, UsdLayer["nomosAttestationGate"]>();
1196
+ const delegationGateByDomain = new Map<string, UsdLayer["nomosDelegationGate"]>();
1176
1197
  const attestedReadGateByDomain = new Map<string, UsdLayer["nomosAttestedReadGate"]>();
1177
1198
  const birthGateByDomain = new Map<string, UsdLayer["nomosBirthGate"]>();
1178
1199
  const birthRecordGateByDomain = new Map<string, UsdLayer["nomosBirthRecordGate"]>();
@@ -1248,6 +1269,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
1248
1269
  if (typeof attGateHex === "string") {
1249
1270
  attestationGateByDomain.set(domain, JSON.parse(unhexUtf8(attGateHex)) as UsdLayer["nomosAttestationGate"]);
1250
1271
  }
1272
+ // THE DELEGATED-AUTHORITY GATE (delegated_authority_births.md): the hex-carried delegation spec.
1273
+ const delGateHex = p.attrs.get("nomos:delegationGateHex");
1274
+ if (typeof delGateHex === "string") {
1275
+ delegationGateByDomain.set(domain, JSON.parse(unhexUtf8(delGateHex)) as UsdLayer["nomosDelegationGate"]);
1276
+ }
1251
1277
  // THE ATTESTED-READ GATE (slice B): the hex-carried trust-edge declaration.
1252
1278
  const attReadGateHex = p.attrs.get("nomos:attestedReadGateHex");
1253
1279
  if (typeof attReadGateHex === "string") {
@@ -1395,6 +1421,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
1395
1421
  const relNs = relationNamespaceByDomain.get(domain);
1396
1422
  const rpNs = readProjectionNamespaceByDomain.get(domain);
1397
1423
  const attGate = attestationGateByDomain.get(domain);
1424
+ const delGate = delegationGateByDomain.get(domain);
1398
1425
  const attReadGate = attestedReadGateByDomain.get(domain);
1399
1426
  const birthGate = birthGateByDomain.get(domain);
1400
1427
  const birthRecordGate = birthRecordGateByDomain.get(domain);
@@ -1423,6 +1450,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
1423
1450
  ...(rpNs !== undefined ? { nomosReadProjectionNamespace: rpNs } : {}),
1424
1451
  ...(relGate !== undefined ? { nomosRelationGate: relGate } : {}),
1425
1452
  ...(attGate !== undefined ? { nomosAttestationGate: attGate } : {}),
1453
+ ...(delGate !== undefined ? { nomosDelegationGate: delGate } : {}),
1426
1454
  ...(attReadGate !== undefined ? { nomosAttestedReadGate: attReadGate } : {}),
1427
1455
  ...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
1428
1456
  ...(birthRecordGate !== undefined ? { nomosBirthRecordGate: birthRecordGate } : {}),