@githolon/dsl 0.90.1 → 0.92.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.
@@ -400,6 +400,46 @@ class EncryptionRosterMismatch implements Exception {
400
400
  'EncryptionRosterMismatch: roster for "$scope" != expected (the fan-out gate would refuse): missing $missing, unexpected $extra';
401
401
  }
402
402
 
403
+ /// Thrown when a `.births()` offer ADMITTED on the parent (a head, a 200) but a child workspace did NOT come
404
+ /// up — its `birthOutcomes` entry carried a `status` other than "born" (e.g. `"failed"`/`"refused"` with code
405
+ /// `BirthLawPackageUnavailable`, when the child's law package is not resolvable from the parent's custody).
406
+ /// Without this, the app would open the child and poll `head:null` until timeout with no reason. Callers catch
407
+ /// it to see the child name(s), the kernel [code], and the [reason]. [head] is the parent write that landed.
408
+ class BirthIncomplete implements Exception {
409
+ /// The raw failed birth outcomes (`{ workspace, status, reason, code }`), status != "born".
410
+ final List<Map<String, Object?>> failedBirths;
411
+
412
+ /// The parent write's local head — the parent offer itself sealed; only the child did not come up.
413
+ final String head;
414
+
415
+ BirthIncomplete(this.failedBirths, {this.head = ''});
416
+
417
+ /// The first failed child's kernel code (e.g. `BirthLawPackageUnavailable`), or `BirthFailed`.
418
+ String get code =>
419
+ (failedBirths.isNotEmpty ? failedBirths.first['code']?.toString() : null) ?? 'BirthFailed';
420
+
421
+ /// The child workspace name(s) that did not come up.
422
+ List<String> get workspaces => failedBirths
423
+ .map((o) => (o['workspace'] ?? '').toString())
424
+ .where((s) => s.isNotEmpty)
425
+ .toList();
426
+
427
+ @override
428
+ String toString() {
429
+ final list = failedBirths
430
+ .map((o) =>
431
+ '${o['workspace'] ?? '?'} [${o['status']}${o['code'] != null ? ': ${o['code']}' : ''}]'
432
+ '${o['reason'] != null ? ' — ${o['reason']}' : ''}')
433
+ .join('; ');
434
+ final lawUnavailable =
435
+ failedBirths.any((o) => o['code'] == 'BirthLawPackageUnavailable');
436
+ return 'BirthIncomplete: ${failedBirths.length} child workspace(s) did NOT come up — $list. '
437
+ 'The parent offer admitted, but the child was not fully born, so opening it would poll head:null '
438
+ 'until timeout. ${lawUnavailable ? "The child's law package is not resolvable from the parent's "
439
+ "custody — deploy it to the parent (POST /v2/workspaces/<parent>/domains) BEFORE birthing." : "See the outcome's reason/code."}';
440
+ }
441
+ }
442
+
403
443
  /// The outcome of an epoch-advancing ceremony verb: the new local [head], the [epoch] opened, and the
404
444
  /// [roster] the key was wrapped to.
405
445
  class NomosEpochResult {
@@ -1348,6 +1388,16 @@ class NomosBridge {
1348
1388
  .whereType<Map>()
1349
1389
  .map((m) => m.cast<String, Object?>())
1350
1390
  .toList();
1391
+ // FAIL FAST on a birth that did NOT come up. The JS holon (index.mjs) already throws on this, so we
1392
+ // normally never see it here — but a runner/bundle skew (an older runner that passes birthOutcomes
1393
+ // through without throwing) must NOT let a silent partial birth reach the app to strand on head:null.
1394
+ // A `status` other than "born" is a failure; throw the typed [BirthIncomplete].
1395
+ final failedBirths = births
1396
+ .where((m) => m['status'] != null && m['status'] != 'born')
1397
+ .toList();
1398
+ if (failedBirths.isNotEmpty) {
1399
+ throw BirthIncomplete(failedBirths, head: (r['head'] ?? '').toString());
1400
+ }
1351
1401
  // `born` elements may be plain workspace-name strings OR `{workspace,…}` objects (both shapes ship);
1352
1402
  // fall back to the birth outcomes' workspace names when no bare `born` list was sent.
1353
1403
  final bornRaw = (r['born'] as List?);
package/dart/pubspec.yaml CHANGED
@@ -11,7 +11,7 @@ description: >-
11
11
  Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
12
12
  code-generated per-domain aggregate + directive-payload types. (De)serializes
13
13
  the EXACT serde JSON the Rust kernel + projection produce/consume.
14
- version: 0.57.0
14
+ version: 0.58.0
15
15
 
16
16
  environment:
17
17
  sdk: ^3.5.0
@@ -20,7 +20,7 @@ dependencies:
20
20
  # The pure-data core (wire types, offer codec, NomosReads, schema validation, error
21
21
  # info). nomos_client depends on and re-exports it, so existing imports are unchanged.
22
22
  # For local monorepo dev, pubspec_overrides.yaml points this at ../dart_types.
23
- nomos_types: ^0.4.1
23
+ nomos_types: ^0.5.0
24
24
  json_schema: ^5.2.2
25
25
  crypto: ^3.0.0
26
26
 
@@ -26,6 +26,9 @@ NomosBridgeError nomosErrorFromInfo(NomosErrorInfo info) {
26
26
  if (info.code.startsWith('nomos.authz.')) return AuthzRefusedError.fromInfo(info);
27
27
  if (info.code.startsWith('nomos.auth.')) return AuthError.fromInfo(info);
28
28
  if (info.code.startsWith('nomos.load.')) return LoadError.fromInfo(info);
29
+ if (info.code == 'nomos.attested_read.source_not_mounted') {
30
+ return SourceSessionNotMounted.fromInfo(info);
31
+ }
29
32
  return NomosBridgeError(info.message, info: info);
30
33
  }
31
34
 
@@ -62,6 +65,11 @@ class NomosRecommendedAction {
62
65
  /// [ParentSignerEnrollmentMissing] (a reborn parent that lost the signer tuple).
63
66
  static const ensureParentSignerEnrollment = 'ensureParentSignerEnrollment';
64
67
 
68
+ /// Mount the SOURCE workspace of a colocated attested read as a session on this device's plane
69
+ /// (`bridge.connect(session: <sourceWs>)` / `ref.acquire(bridge)`) — the ONE remedy for
70
+ /// [SourceSessionNotMounted] (a directive's `read(q, {from})` resolves in-engine from the mounted source).
71
+ static const mountSourceSession = 'mountSourceSession';
72
+
65
73
  /// No automatic remedy — surface to a human.
66
74
  static const none = 'none';
67
75
 
@@ -84,6 +92,8 @@ class NomosRecommendedAction {
84
92
  return 'reopen the holon (reconnect fresh)';
85
93
  case ensureParentSignerEnrollment:
86
94
  return 'restore the device signer on the parent (ensureParentSignerEnrollment / NomosScope.home repairs it on reopen)';
95
+ case mountSourceSession:
96
+ return 'mount the attested read\'s source workspace as a session on this plane (connect it / ref.acquire) so the read resolves in-engine';
87
97
  case none:
88
98
  return 'no automatic remedy — inspect the error';
89
99
  default:
@@ -413,6 +423,63 @@ class LoadError extends NomosBridgeError {
413
423
  '${sourceUrl != null ? ', sourceUrl: $sourceUrl' : ''}, recommendedAction: $recommendedAction): $message';
414
424
  }
415
425
 
426
+ /// SOURCE SESSION NOT MOUNTED (`nomos.attested_read.source_not_mounted`) — a directive whose law declares a
427
+ /// colocated foreign read (`read(query, {from: <sourceWs>})`) was offered while the SOURCE workspace was NOT
428
+ /// mounted as a session on this device's engine plane. A colocated attested read resolves + signs IN-ENGINE
429
+ /// (zero HTTP) from the mounted source; when the source is absent the bridge refuses IMMEDIATELY (this error)
430
+ /// instead of a doomed ~31s remote fetch that answers an unsigned projection. The remedy is
431
+ /// [NomosRecommendedAction.mountSourceSession]: open [sourceWorkspace] as a session on the SAME bridge/realm
432
+ /// (`bridge.connect(session: sourceWorkspace)` / `ref.acquire(bridge)`) BEFORE offering the directive.
433
+ class SourceSessionNotMounted extends NomosBridgeError {
434
+ /// The stable machine code — always `nomos.attested_read.source_not_mounted`.
435
+ final String code;
436
+
437
+ /// The name the directive's `read(..., {from})` (or explicit attested read) referenced — a session alias
438
+ /// or a workspace name.
439
+ final String? from;
440
+
441
+ /// The resolved SOURCE workspace that must be mounted on this plane.
442
+ final String? sourceWorkspace;
443
+
444
+ /// The declared query the foreign read names, when known.
445
+ final String? queryId;
446
+
447
+ /// The named remedy — [NomosRecommendedAction.mountSourceSession].
448
+ final String recommendedAction;
449
+
450
+ SourceSessionNotMounted({
451
+ this.code = 'nomos.attested_read.source_not_mounted',
452
+ required String message,
453
+ this.from,
454
+ this.sourceWorkspace,
455
+ this.queryId,
456
+ this.recommendedAction = NomosRecommendedAction.mountSourceSession,
457
+ NomosErrorInfo? info,
458
+ }) : super(message, info: info);
459
+
460
+ factory SourceSessionNotMounted.fromInfo(NomosErrorInfo info) {
461
+ String? attr(String name) {
462
+ final v = info.attributes['nomos.attested_read.$name'] ?? info.attributes[name];
463
+ return v?.toString();
464
+ }
465
+
466
+ return SourceSessionNotMounted(
467
+ code: info.code,
468
+ message: info.message,
469
+ from: attr('from'),
470
+ sourceWorkspace: attr('source_workspace') ?? attr('sourceWorkspace') ?? info.workspace,
471
+ queryId: (info.attributes['nomos.query_id'] ?? attr('queryId'))?.toString(),
472
+ recommendedAction: attr('recommendedAction') ?? attr('recommended_action') ?? NomosRecommendedAction.mountSourceSession,
473
+ info: info,
474
+ );
475
+ }
476
+
477
+ @override
478
+ String toString() =>
479
+ 'SourceSessionNotMounted(source: $sourceWorkspace'
480
+ '${queryId != null ? ', query: $queryId' : ''}, recommendedAction: $recommendedAction): $message';
481
+ }
482
+
416
483
  /// The custody phase a [CustodyError] was raised in.
417
484
  enum CustodyPhase { fold, author, fetch, unknown }
418
485
 
@@ -11,7 +11,7 @@ description: >-
11
11
  Pure kernel wire/data types for Nomos Dart frontends — no transport, no bridge,
12
12
  no runtime. The hand-written serde-JSON-compatible types plus the offer codec,
13
13
  reactive read engine, schema validation, and data-only error/trace types.
14
- version: 0.4.1
14
+ version: 0.5.0
15
15
 
16
16
  environment:
17
17
  sdk: ^3.5.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.90.1",
3
+ "version": "0.92.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",
@@ -240,6 +240,13 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
240
240
  const gateDoc = gate !== undefined
241
241
  ? ` /// GATED: needs an \`actor\` (the \`${gate.relation}\` relation) — throws MissingOfferAuthority otherwise.`
242
242
  : "";
243
+ // FALLBACK ATTESTATION INJECTION (co2): a directive whose law declares a foreign read (`.readsFrom(...)`)
244
+ // accepts pre-fetched signed envelopes as an optional `attestations:` param — the manual override when the
245
+ // colocation resolver can't serve the read (the source isn't mounted on the plane). The in-engine
246
+ // colocation path stays the default; only foreign-read directives carry the extra named param.
247
+ const wantAttest = (((d as { declaredForeignReads?: unknown[] }).declaredForeignReads?.length ?? 0) > 0);
248
+ const attNamed = wantAttest ? "List<Map<String, Object?>>? attestations" : "";
249
+ const attArg = wantAttest ? ", attestations: attestations" : "";
243
250
  // E2E SEALING (the sealing client): a directive with `.encrypted()` payload fields SEALS them via the
244
251
  // bound [keyStore] before dispatch (a missing key throws [EncryptionKeyUnavailable] — never a plaintext
245
252
  // write). One async block covers creates + mutate + gated + stamps; `seal: false` is the escape hatch.
@@ -255,7 +262,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
255
262
  ` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the typed [NomosOfferOutcome].${createsDoc}`,
256
263
  ` /// Encrypted field(s) are SEALED via the bound [keyStore] before dispatch (pass \`seal: false\` to skip).${stampsDoc}`,
257
264
  ...(gateDoc ? [gateDoc] : []),
258
- ` Future<NomosOfferOutcome> ${method}(${payloadClass} payload, {bool seal = true}) async {`,
265
+ ` Future<NomosOfferOutcome> ${method}(${payloadClass} payload, {bool seal = true${wantAttest ? ", " + attNamed : ""}}) async {`,
259
266
  ...(gateGuard ? [gateGuard] : []),
260
267
  ` final data = payload.toPayloadJson();`,
261
268
  ...(stamps.length > 0
@@ -265,7 +272,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
265
272
  ]
266
273
  : []),
267
274
  ` await _sealPayload(data, ${dartStr(d.id)}, seal);`,
268
- ` return _offer(${dartStr(d.id)}, data);`,
275
+ ` return _offer(${dartStr(d.id)}, data${attArg});`,
269
276
  ` }`,
270
277
  ``,
271
278
  );
@@ -275,12 +282,12 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
275
282
  methods.push(
276
283
  ` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the typed [NomosOfferOutcome].${createsDoc}${stampsDoc}`,
277
284
  ...(gateDoc ? [gateDoc] : []),
278
- ` Future<NomosOfferOutcome> ${method}(${payloadClass} payload) {`,
285
+ ` Future<NomosOfferOutcome> ${method}(${payloadClass} payload${wantAttest ? ", {" + attNamed + "}" : ""}) {`,
279
286
  ...(gateGuard ? [gateGuard] : []),
280
287
  ` final data = payload.toPayloadJson();`,
281
288
  ` final now = DateTime.now().toUtc().toIso8601String();`,
282
289
  ...stamps.map((s) => ` data.putIfAbsent(${dartStr(s)}, () => now);`),
283
- ` return _offer(${dartStr(d.id)}, data);`,
290
+ ` return _offer(${dartStr(d.id)}, data${attArg});`,
284
291
  ` }`,
285
292
  ``,
286
293
  );
@@ -289,9 +296,9 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
289
296
  methods.push(
290
297
  ` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the typed [NomosOfferOutcome].${createsDoc}`,
291
298
  gateDoc,
292
- ` Future<NomosOfferOutcome> ${method}(${payloadClass} payload) {`,
299
+ ` Future<NomosOfferOutcome> ${method}(${payloadClass} payload${wantAttest ? ", {" + attNamed + "}" : ""}) {`,
293
300
  gateGuard,
294
- ` return _offer(${dartStr(d.id)}, payload.toPayloadJson());`,
301
+ ` return _offer(${dartStr(d.id)}, payload.toPayloadJson()${attArg});`,
295
302
  ` }`,
296
303
  ``,
297
304
  );
@@ -299,8 +306,8 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
299
306
  methods.push(
300
307
  ` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the typed [NomosOfferOutcome].${createsDoc}`,
301
308
  ...(gateDoc ? [gateDoc] : []),
302
- ` Future<NomosOfferOutcome> ${method}(${payloadClass} payload) =>`,
303
- ` _offer(${dartStr(d.id)}, payload.toPayloadJson());`,
309
+ ` Future<NomosOfferOutcome> ${method}(${payloadClass} payload${wantAttest ? ", {" + attNamed + "}" : ""}) =>`,
310
+ ` _offer(${dartStr(d.id)}, payload.toPayloadJson()${attArg});`,
304
311
  ``,
305
312
  );
306
313
  }
@@ -879,9 +886,9 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
879
886
  ``,
880
887
  ` /// THE ONE DISPATCH LANE every generated directive method routes through: lazy preflight, then`,
881
888
  ` /// the session's offer (the generated-internal use of the engine-level lane).`,
882
- ` Future<NomosOfferOutcome> _offer(String directiveId, Object? payload) async {`,
889
+ ` Future<NomosOfferOutcome> _offer(String directiveId, Object? payload, {List<Map<String, Object?>>? attestations}) async {`,
883
890
  ` await _preflightOnce();`,
884
- ` return bridge.offerOutcome(domain: ${dartStr(domain)}, directiveId: directiveId, payload: payload, actor: actor, domainHash: domainHash);`,
891
+ ` return bridge.offerOutcome(domain: ${dartStr(domain)}, directiveId: directiveId, payload: payload, actor: actor, domainHash: domainHash, attestations: attestations);`,
885
892
  ` }`,
886
893
  ``,
887
894
  ...(encPresent ? dartEncClassMembers(encMap!) : []),
package/src/codegen_ts.ts CHANGED
@@ -1012,7 +1012,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1012
1012
  ` /** THE ONE ENCODER offer (the single v2-envelope codec lives in @githolon/client \`encodeIntentOfferBytes\`):`,
1013
1013
  ` * hand the holon a PLANNED directive + the verified \`actor\`; the holon encodes + offers. The generated`,
1014
1014
  ` * client calls THIS, never building offer bytes itself — so there is exactly ONE copy of the wire format. */`,
1015
- ` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string; returnCreated?: boolean }): Promise<string | { head: string; created?: { type: string; id: string }[]; workspace?: string }>;`,
1015
+ ` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string; returnCreated?: boolean; attestations?: readonly { queryId: string; result: unknown; attestation: unknown }[] }): Promise<string | { head: string; created?: { type: string; id: string }[]; workspace?: string }>;`,
1016
1016
  ` /** Opaque-bytes offer — retained for carriers that already hold an encoded envelope (back-compat). */`,
1017
1017
  ` offer(intentBytes: string | Uint8Array): Promise<string>;`,
1018
1018
  ...(hasBirths
@@ -1849,7 +1849,22 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1849
1849
  // escape hatch, and `pv` (the sealed copy) replaces `payload` downstream. Non-encrypted directives
1850
1850
  // are byte-identical (wantSeal === false ⇒ no prelude, no extra param, plain `payload`).
1851
1851
  const wantSeal = encMap?.[domain]?.directives?.[d.id] !== undefined;
1852
- const paramSig = wantSeal ? `payload: ${payloadName}, callOpts?: { seal?: boolean }` : `payload: ${payloadName}`;
1852
+ // FALLBACK ATTESTATION INJECTION (co2): a directive whose law declares a foreign read
1853
+ // (`.readsFrom(...)`) accepts pre-fetched signed envelopes as `callOpts.attestations` — the manual
1854
+ // escape hatch when the colocation resolver can't serve the read (the source isn't on the plane). The
1855
+ // in-engine colocation path (the offer resolves the read from the mounted source, zero HTTP) remains
1856
+ // the default; this is the explicit override a caller reaches for. Emitted ONLY for foreign-read
1857
+ // directives (an ordinary directive's signature is byte-identical).
1858
+ const wantAttest = (((d as { declaredForeignReads?: unknown[] }).declaredForeignReads?.length ?? 0) > 0);
1859
+ const attTy = `readonly { queryId: string; result: unknown; attestation: unknown }[]`;
1860
+ const callOptsFields = [
1861
+ ...(wantSeal ? ["seal?: boolean"] : []),
1862
+ ...(wantAttest ? [`attestations?: ${attTy}`] : []),
1863
+ ];
1864
+ const paramSig = callOptsFields.length
1865
+ ? `payload: ${payloadName}, callOpts?: { ${callOptsFields.join("; ")} }`
1866
+ : `payload: ${payloadName}`;
1867
+ const offerExtra = wantAttest ? ", attestations: callOpts?.attestations" : "";
1853
1868
  const sealPrelude = wantSeal
1854
1869
  ? ` const _sealed = await _sealFields(holon, keyStore, ${encConst}[${JSON.stringify(domain)}].directives[${JSON.stringify(d.id)}] as Record<string, { scope: string; aggSid: string; fieldSid: string }>, payload as Record<string, unknown>, callOpts?.seal !== false) as ${payloadName};`
1855
1870
  : "";
@@ -1907,7 +1922,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1907
1922
  ...(gateGuard ? [gateGuard.replace(/\n$/, "")] : []),
1908
1923
  ...(sealPrelude ? [sealPrelude] : []),
1909
1924
  ` const ${mint.field} = payload.${mint.field} ?? (await (holon as NomosRoutedHolon).mint(${JSON.stringify(mint.mintType)}${mintOpts}));`,
1910
- ` return _outcome(holon.workspace, holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, { domainHash, actor, returnCreated: true }) as Promise<string | { head: string; created?: { type: string; id: string }[] }>, { workspace: holon.workspace, type: ${JSON.stringify(dd.aggregateId ?? mint.mintType)}, id: ${mint.field} });`,
1925
+ ` return _outcome(holon.workspace, holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, { domainHash, actor, returnCreated: true${offerExtra} }) as Promise<string | { head: string; created?: { type: string; id: string }[] }>, { workspace: holon.workspace, type: ${JSON.stringify(dd.aggregateId ?? mint.mintType)}, id: ${mint.field} });`,
1911
1926
  ` },`,
1912
1927
  );
1913
1928
  continue;
@@ -1924,7 +1939,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1924
1939
  ` ${d.id}: async (${paramSig}): Promise<NomosOutcome> => {`,
1925
1940
  ...(gateGuard ? [gateGuard.replace(/\n$/, "")] : []),
1926
1941
  ...(sealPrelude ? [sealPrelude] : []),
1927
- ` return _outcome(holon.workspace, holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${offerPayload}, { domainHash, actor, returnCreated: true }) as Promise<string | { head: string; created?: { type: string; id: string }[] }>);`,
1942
+ ` return _outcome(holon.workspace, holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${offerPayload}, { domainHash, actor, returnCreated: true${offerExtra} }) as Promise<string | { head: string; created?: { type: string; id: string }[] }>);`,
1928
1943
  ` },`,
1929
1944
  );
1930
1945
  }
@@ -1046,11 +1046,11 @@ const DART_TYPES_IMPORT = `import 'package:nomos_types/nomos_types.dart';`;
1046
1046
  * just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
1047
1047
  // 0.3.0: SurfacedValue<T>/SurfacedObservation/PendingConflictRow — generated Surfaced-field typing +
1048
1048
  // the pendingConflicts read rows.
1049
- const NOMOS_TYPES_DEP = "^0.4.1";
1049
+ const NOMOS_TYPES_DEP = "^0.5.0";
1050
1050
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1051
1051
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1052
1052
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1053
- const NOMOS_CLIENT_DEP = "^0.57.0";
1053
+ const NOMOS_CLIENT_DEP = "^0.58.0";
1054
1054
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1055
1055
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1056
1056
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -86,6 +86,17 @@ import { signerObject, SIGNER_RELATION } from "./signing_identity.js";
86
86
  // them up as part of the `workspaces` domain. (architecture/platform_birth_genesis_bootstrap.md)
87
87
  export { GenesisWarrant, seedGenesis } from "./genesis.js";
88
88
  export { identityRelations, enrollSigner } from "./signing_identity.js";
89
+ // THE BIRTH-ATTESTATION CARRIER (the K_root attestation spine for platform-hosted homes). A platform must
90
+ // CARRY its own `platform_cert` at `birth-attestation:self` so the kernel's `assemble_home_cert_chain`
91
+ // PREPENDS `[home_cert, delegation]` to it and the child home roots at K_root. Governance carries the cert
92
+ // (this trio) but DELIBERATELY does NOT export the `parentAttestedBirth` era marker — a platform's own 1-link
93
+ // `[platform_cert]` chain would fail `verify_to_root` (>=2 links), so its own verify doors must stay inert
94
+ // while it still carries the cert for its children. Fully additive (new aggregate + directive, no era key ⇒
95
+ // pre-existing chains replay byte-identical). architecture/co2_attestation_spine.md.
96
+ export { BIRTH_ATTESTATION_ID, BirthAttestation, birthAttestation, seedBirthAttestation } from "./home.js";
97
+ // THE ENROLL-ONCE DELEGATION LANE — a platform records each tenant's device-key delegation (signed by its
98
+ // onboarding signer S_P) so the kernel reads it at `signer-delegation:<user>` when assembling a home's chain.
99
+ export { SignerDelegation, enrollDelegation, signerDelegationByUser } from "./signing_identity.js";
89
100
 
90
101
  // ── WORKSPACE SELF-IDENTITY (the law reads its OWN install-context) ──────────────────────────────────
91
102
  // The governance bundle runs on root AND every born platform. A workspace knows its ROLE via this