@githolon/dsl 0.91.0 → 0.93.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dart/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
 
@@ -1,3 +1,7 @@
1
+ ## 0.5.0
2
+
3
+ - Add typed `SourceSessionNotMounted` error (code `nomos.attested_read.source_not_mounted`) for the colocated attested-read fix — a foreign `read(q, {from})` whose source workspace isn't mounted now fails fast with a typed error + `mountSourceSession` remedy.
4
+
1
5
  ## 0.4.1
2
6
 
3
7
  - `canonicalPrincipal` / `signerObjectOf` — THE one principal normalizer (kernel truth:
@@ -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.91.0",
3
+ "version": "0.93.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
@@ -1086,6 +1086,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1086
1086
  ` * and the holon computes the LIVE per-member verdict on \`holon.compat\`. A KNOWN-incompatible directive`,
1087
1087
  ` * then refuses CLIENT-SIDE at write time (LawMismatchError), never as a server 422. */`,
1088
1088
  ` checkCompat?(compat: { lawHash: string; interfaceHash: string; packageName: string }, requirement?: unknown): NomosCompatVerdict | null;`,
1089
+ ` /** RELEASE COHERENCE: throws a typed KernelCoherenceError at bind if the cloud's kernel lacks a`,
1090
+ ` * capability this law requires (e.g. \`compoundKeys\`) — instead of an opaque deep fold refusal. */`,
1091
+ ` assertKernelCoherence?(requiresKernel: readonly string[], opts?: { lawHash?: string }): unknown;`,
1089
1092
  ` /** The live compat verdict (set by checkCompat). \`has(memberId)\` is the dark-launch feature flag;`,
1090
1093
  ` * \`refresh()\` re-derives against the current projection. Absent until the factory binds. */`,
1091
1094
  ` compat?: NomosCompatVerdict;`,
@@ -1849,7 +1852,22 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1849
1852
  // escape hatch, and `pv` (the sealed copy) replaces `payload` downstream. Non-encrypted directives
1850
1853
  // are byte-identical (wantSeal === false ⇒ no prelude, no extra param, plain `payload`).
1851
1854
  const wantSeal = encMap?.[domain]?.directives?.[d.id] !== undefined;
1852
- const paramSig = wantSeal ? `payload: ${payloadName}, callOpts?: { seal?: boolean }` : `payload: ${payloadName}`;
1855
+ // FALLBACK ATTESTATION INJECTION (co2): a directive whose law declares a foreign read
1856
+ // (`.readsFrom(...)`) accepts pre-fetched signed envelopes as `callOpts.attestations` — the manual
1857
+ // escape hatch when the colocation resolver can't serve the read (the source isn't on the plane). The
1858
+ // in-engine colocation path (the offer resolves the read from the mounted source, zero HTTP) remains
1859
+ // the default; this is the explicit override a caller reaches for. Emitted ONLY for foreign-read
1860
+ // directives (an ordinary directive's signature is byte-identical).
1861
+ const wantAttest = (((d as { declaredForeignReads?: unknown[] }).declaredForeignReads?.length ?? 0) > 0);
1862
+ const attTy = `readonly { queryId: string; result: unknown; attestation: unknown }[]`;
1863
+ const callOptsFields = [
1864
+ ...(wantSeal ? ["seal?: boolean"] : []),
1865
+ ...(wantAttest ? [`attestations?: ${attTy}`] : []),
1866
+ ];
1867
+ const paramSig = callOptsFields.length
1868
+ ? `payload: ${payloadName}, callOpts?: { ${callOptsFields.join("; ")} }`
1869
+ : `payload: ${payloadName}`;
1870
+ const offerExtra = wantAttest ? ", attestations: callOpts?.attestations" : "";
1853
1871
  const sealPrelude = wantSeal
1854
1872
  ? ` 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
1873
  : "";
@@ -1907,7 +1925,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1907
1925
  ...(gateGuard ? [gateGuard.replace(/\n$/, "")] : []),
1908
1926
  ...(sealPrelude ? [sealPrelude] : []),
1909
1927
  ` 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} });`,
1928
+ ` 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
1929
  ` },`,
1912
1930
  );
1913
1931
  continue;
@@ -1924,7 +1942,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1924
1942
  ` ${d.id}: async (${paramSig}): Promise<NomosOutcome> => {`,
1925
1943
  ...(gateGuard ? [gateGuard.replace(/\n$/, "")] : []),
1926
1944
  ...(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 }[] }>);`,
1945
+ ` 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
1946
  ` },`,
1929
1947
  );
1930
1948
  }
@@ -2094,6 +2112,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
2094
2112
  opts.interface !== undefined
2095
2113
  ? ` holon.checkCompat?.(${compatConstName(opts.packageName)}, ${interfaceConstName(opts.packageName)});`
2096
2114
  : ` holon.checkCompat?.(${compatConstName(opts.packageName)});`,
2115
+ // RELEASE COHERENCE: throw a typed skew error AT BIND if the cloud's kernel lacks a capability
2116
+ // this law requires (e.g. compoundKeys) — instead of the opaque deep fold refusal later.
2117
+ ` holon.assertKernelCoherence?.(${compatConstName(opts.packageName)}.requiresKernel ?? [], { lawHash: ${compatConstName(opts.packageName)}.lawHash });`,
2097
2118
  ]
2098
2119
  : []),
2099
2120
  ` return {`,
@@ -17,4 +17,11 @@ export interface NomosCompat {
17
17
  readonly domains: Record<string, { readonly identityHash?: string }>;
18
18
  /** The generator provenance — the `@githolon/dsl` version + this manifest's schema. */
19
19
  readonly generator: { readonly dsl: string; readonly schema: number };
20
+ /** RELEASE COHERENCE (kernel_capabilities.ts): the named kernel capabilities this law
21
+ * REQUIRES to fold (derived from its shape — e.g. `compoundKeys` for a `.key(a, b)` query).
22
+ * connect() / deploy compares this against the target kernel's advertised capabilities and
23
+ * fails with a TYPED message naming the mismatch, INSTEAD of the opaque deep-fold refusal.
24
+ * Empty ⇒ no special capability needed (byte-identical to before this field for such a law
25
+ * — but note: this manifest is frontend-only, never in the USD IR / domainHash). */
26
+ readonly requiresKernel: readonly string[];
20
27
  }
@@ -132,6 +132,7 @@ import { domainInterface, type DomainInterface } from "./shape_compat.js";
132
132
  import { generateTsProof, proofLegsIndex } from "./codegen_proof.js";
133
133
  import { checkDomainModules, formatFindings, hasRefusal } from "./law_check.js";
134
134
  import type { NomosCompat } from "./compat_manifest.js";
135
+ import { requiredKernelFeatures, type FeatureProbeModule } from "./kernel_capabilities.js";
135
136
 
136
137
  const DSL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
137
138
 
@@ -1046,11 +1047,11 @@ const DART_TYPES_IMPORT = `import 'package:nomos_types/nomos_types.dart';`;
1046
1047
  * just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
1047
1048
  // 0.3.0: SurfacedValue<T>/SurfacedObservation/PendingConflictRow — generated Surfaced-field typing +
1048
1049
  // the pendingConflicts read rows.
1049
- const NOMOS_TYPES_DEP = "^0.4.1";
1050
+ const NOMOS_TYPES_DEP = "^0.5.0";
1050
1051
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1051
1052
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1052
1053
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1053
- const NOMOS_CLIENT_DEP = "^0.57.0";
1054
+ const NOMOS_CLIENT_DEP = "^0.58.0";
1054
1055
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1055
1056
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1056
1057
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -2552,12 +2553,26 @@ async function main(): Promise<void> {
2552
2553
  const identityHash = identity.hashes[key];
2553
2554
  compatDomains[key] = identityHash === undefined ? {} : { identityHash };
2554
2555
  }
2556
+ // RELEASE COHERENCE — the kernel capabilities this law REQUIRES (compound-key queries +
2557
+ // era-key features). Baked into the client so connect() can check it against the target
2558
+ // kernel's advertised capabilities BEFORE the law folds (kernel_capabilities.ts).
2559
+ const requiresKernel = ((): string[] => {
2560
+ const probes: FeatureProbeModule[] = domainModules.map((m) => (m.queries ? { queries: m.queries } : {}));
2561
+ try {
2562
+ const parsed = JSON.parse(usdJson) as { layers?: Record<string, unknown>[] };
2563
+ for (const layer of parsed.layers ?? []) probes.push({ layer });
2564
+ } catch {
2565
+ /* shape-derived features (compoundKeys) still apply from the queries above */
2566
+ }
2567
+ return requiredKernelFeatures(probes);
2568
+ })();
2555
2569
  const nomosCompat: NomosCompat = {
2556
2570
  packageName: cfg.name,
2557
2571
  lawHash: domainHash,
2558
2572
  interfaceHash: sha256HexUtf8(canonicalJson(packageInterface)),
2559
2573
  domains: compatDomains,
2560
2574
  generator: { dsl: dslVersion, schema: 1 },
2575
+ requiresKernel,
2561
2576
  };
2562
2577
 
2563
2578
  // ── 3b. the OpenAPI spec — THE PACKAGE'S SELF-DESCRIPTION (the host's runtime routing table;
@@ -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
package/src/index.ts CHANGED
@@ -148,6 +148,20 @@ export { repair, repairsOf, isRepairDecl, type RepairDecl, type RepairMatcher, t
148
148
  // `nomos-compile` REFUSES law that references read() (a named compile error, never a runtime halt).
149
149
  // The captured-reads wave (task #58) wires the capability in and lifts the refusal.
150
150
  export { read } from "./read.js";
151
+ // RELEASE COHERENCE — the kernel capability stamp + law↔kernel fit check (kernel_capabilities.ts).
152
+ export {
153
+ requiredKernelFeatures,
154
+ capabilitiesForKernelHash,
155
+ assessKernelCoherence,
156
+ KernelCoherenceError,
157
+ KERNEL_CAPABILITY_REGISTRY,
158
+ ERA_KEY_TO_FEATURE,
159
+ type KernelFeature,
160
+ type KernelStamp,
161
+ type KernelCoherenceVerdict,
162
+ type KernelRegistryEntry,
163
+ type FeatureProbeModule,
164
+ } from "./kernel_capabilities.js";
151
165
  // ATTESTED CROSS-WORKSPACE READS (slice B — attested_reads_design.md): the declared trust
152
166
  // surface (`trustSource`), the foreign query reference, and the reader-side SourceFrontier.
153
167
  export {
@@ -0,0 +1,245 @@
1
+ // RELEASE COHERENCE — the kernel capability stamp + the law↔kernel fit check.
2
+ //
3
+ // THE PROBLEM THIS SOLVES (the co2 skew, 2026-07): a published release can be
4
+ // internally inconsistent. The production kernel accepts law that a compiler emits;
5
+ // the DEV/test-mint kernel a `githolon cloud dev` serves can be STALE and REFUSE the
6
+ // same law — but only at runtime, deep in a law fold ("query declares 2 index keys;
7
+ // the read router supports exactly one"). Four artifacts drift independently:
8
+ // · the compiler (@githolon/dsl) that EMITS the law,
9
+ // · the runner bundle (@githolon/client) that RUNS it,
10
+ // · the production kernel wasm the cloud serves,
11
+ // · the test-mint kernel wasm the dev cloud serves.
12
+ // There is NO version/hash check that catches the skew before the opaque refusal.
13
+ //
14
+ // THE FIX: a kernel advertises a VERSION + a set of named CAPABILITIES (feature flags
15
+ // keyed to the era keys / read-router lanes it implements). A package DECLARES the
16
+ // kernel capabilities its law REQUIRES (derived from its shape — e.g. a compound-key
17
+ // query needs `compoundKeys`). Before the law ever folds, we compare the two and fail
18
+ // with a TYPED, human-legible message NAMING the mismatch — not the deep refusal.
19
+ //
20
+ // This module is the SINGLE SOURCE OF TRUTH for: the capability vocabulary, deriving a
21
+ // package's requirements from its shape, the known-kernel registry (keyed by wasm
22
+ // sha256), and the coherence assessment. The CLI (`githolon doctor`, deploy), the
23
+ // generated client (baked `requiresKernel`), and the cloud worker's
24
+ // `/v1/runtime/capabilities` endpoint all draw on it.
25
+
26
+ import type { QueryDecl } from "./query.js";
27
+
28
+ /** A named kernel capability — a feature/lane the wasm kernel must implement for a law
29
+ * that uses it to fold. Each maps to a read-router lane or an era key. Additive: a new
30
+ * capability name is minted here the same day the kernel arm ships. */
31
+ export type KernelFeature =
32
+ /** The read router accepts a query with MORE THAN ONE index key (`.key(a, b, …)`),
33
+ * routing it as a compound (AND) intersection. Pre-this kernels refuse >1 index key
34
+ * ("the read router supports exactly one"). THE CONCRETE co2 SKEW. */
35
+ | "compoundKeys"
36
+ /** The #58 captured-read lane (`read()` in deployable law + `.reads(query)`). */
37
+ | "capturedReads"
38
+ /** The #41 declared-invariant gate (`invariant` / `workspaceInvariant`). */
39
+ | "invariantGate"
40
+ /** Surfaced(inner) — retained concurrent competing values as a pending set. */
41
+ | "surfacedMerge"
42
+ /** `.attributed()` / aggregate `{attributed:true}` per-field provenance. */
43
+ | "fieldProvenance"
44
+ /** Null as an opt-in named capability (`admitsNull()`). */
45
+ | "nullSemantics"
46
+ /** Dynamic attested reads under a trustPolicy allowList. */
47
+ | "attestedReads";
48
+
49
+ /** The USD-IR era key each derived-from-law capability rides on, for the requirement
50
+ * derivation below (a package that emits the era key needs the capability). */
51
+ export const ERA_KEY_TO_FEATURE: Readonly<Record<string, KernelFeature>> = {
52
+ nomosReadGate: "capturedReads",
53
+ nomosInvariantGate: "invariantGate",
54
+ nomosSurfaced: "surfacedMerge",
55
+ nomosAttributed: "fieldProvenance",
56
+ nomosNullable: "nullSemantics",
57
+ nomosAttestedReads: "attestedReads",
58
+ };
59
+
60
+ /** A minimal shape of a compiled domain module — enough to derive its kernel needs. */
61
+ export interface FeatureProbeModule {
62
+ readonly queries?: readonly QueryDecl[];
63
+ /** The domain's USD-IR layer (the era keys ride here). Optional — shape-derived
64
+ * features (compoundKeys) work without it. */
65
+ readonly layer?: Record<string, unknown>;
66
+ }
67
+
68
+ /** DERIVE the kernel capabilities a package's law REQUIRES, from its shape + era keys.
69
+ * The result is a sorted, de-duped list baked into the client compat manifest and
70
+ * checked against a target kernel's advertised capabilities before the law folds.
71
+ *
72
+ * The load-bearing case: ANY query with a compound (`.key(a, b)`) index needs
73
+ * `compoundKeys` — exactly the lane the stale test-mint kernel lacked. */
74
+ export function requiredKernelFeatures(modules: readonly FeatureProbeModule[]): KernelFeature[] {
75
+ const needs = new Set<KernelFeature>();
76
+ for (const m of modules) {
77
+ for (const q of m.queries ?? []) {
78
+ if (Array.isArray(q.key) && q.key.length > 1) needs.add("compoundKeys");
79
+ }
80
+ if (m.layer) {
81
+ for (const [eraKey, feature] of Object.entries(ERA_KEY_TO_FEATURE)) {
82
+ if (m.layer[eraKey] !== undefined) needs.add(feature);
83
+ }
84
+ }
85
+ }
86
+ return [...needs].sort();
87
+ }
88
+
89
+ /** A kernel's self-description: its version tag + the capabilities it implements. This is
90
+ * the shape the kernel stamps (see `kernelStamp` op in wasm_git_holon.rs) AND the shape
91
+ * the host `/v1/runtime/capabilities` endpoint synthesizes for the deployed wasm it can't
92
+ * yet ask (keyed by the wasm sha256 in the registry below). */
93
+ export interface KernelStamp {
94
+ /** The kernel semver-ish version tag baked into the wasm at build. */
95
+ readonly version: string;
96
+ /** The wasm artifact sha256 (64-hex), when known. */
97
+ readonly hash?: string;
98
+ /** The named capabilities this kernel implements. */
99
+ readonly features: readonly KernelFeature[];
100
+ /** Human note (e.g. "production", "test-mint (DEV ONLY)") — surfaced in the skew message. */
101
+ readonly note?: string;
102
+ }
103
+
104
+ /** THE KNOWN-KERNEL REGISTRY — maps a kernel wasm sha256 PREFIX to its capability stamp.
105
+ * Keyed by prefix so a short hash (the co2 report cites `846c6bf8` / `f81cb952`) resolves.
106
+ *
107
+ * WHY host-side and not only asked of the kernel: the CURRENTLY-DEPLOYED stale kernel
108
+ * predates the stamp op — it cannot answer "what are your capabilities?". Until every
109
+ * serving kernel carries the in-wasm stamp, this registry lets the host + client resolve
110
+ * a deployed wasm's capabilities from its BYTES, which is always available. A kernel that
111
+ * DOES carry the stamp op is preferred; this is the fallback that works today.
112
+ *
113
+ * Add an entry the moment a new kernel wasm is cut (its sha256 + the features it gained). */
114
+ export interface KernelRegistryEntry extends KernelStamp {
115
+ readonly prefix: string;
116
+ /** Human note (e.g. "production", "test-mint (DEV ONLY)"). */
117
+ readonly note?: string;
118
+ }
119
+
120
+ export const KERNEL_CAPABILITY_REGISTRY: readonly KernelRegistryEntry[] = [
121
+ {
122
+ prefix: "846c6bf8",
123
+ version: "kernel-846c6bf8",
124
+ note: "production (accepts compound-key law)",
125
+ features: [
126
+ "compoundKeys",
127
+ "capturedReads",
128
+ "invariantGate",
129
+ "surfacedMerge",
130
+ "fieldProvenance",
131
+ "nullSemantics",
132
+ "attestedReads",
133
+ ],
134
+ },
135
+ {
136
+ // CURRENT test-mint kernel (rebuilt from the SAME source as prod 846c6bf8, 2026-07-14; staged to R2 +
137
+ // built/staged by deploy.sh every release). Feature-equivalent to production — the dev cloud is coherent.
138
+ prefix: "f6e44b3a",
139
+ version: "kernel-f6e44b3a",
140
+ note: "test-mint (DEV) — current, feature-equivalent to production",
141
+ features: [
142
+ "compoundKeys",
143
+ "capturedReads",
144
+ "invariantGate",
145
+ "surfacedMerge",
146
+ "fieldProvenance",
147
+ "nullSemantics",
148
+ "attestedReads",
149
+ ],
150
+ },
151
+ {
152
+ prefix: "f81cb952",
153
+ version: "kernel-f81cb952",
154
+ note: "test-mint (DEV ONLY — STALE: predates compound-key read router; superseded by f6e44b3a)",
155
+ features: [
156
+ "capturedReads",
157
+ "invariantGate",
158
+ "surfacedMerge",
159
+ "fieldProvenance",
160
+ "nullSemantics",
161
+ "attestedReads",
162
+ ],
163
+ },
164
+ ];
165
+
166
+ /** Resolve a kernel wasm hash (full or short) to its known capability stamp, or `null`
167
+ * when the hash is unknown to the registry (a kernel we've never catalogued). */
168
+ export function capabilitiesForKernelHash(hash: string | undefined | null): KernelStamp | null {
169
+ if (typeof hash !== "string" || hash.length < 6) return null;
170
+ const h = hash.toLowerCase().replace(/^sha256:|^policy:/, "");
171
+ const hit = KERNEL_CAPABILITY_REGISTRY.find((e) => h.startsWith(e.prefix) || e.prefix.startsWith(h));
172
+ return hit
173
+ ? { version: hit.version, hash: h, features: hit.features, ...(hit.note ? { note: hit.note } : {}) }
174
+ : null;
175
+ }
176
+
177
+ /** The verdict of a law↔kernel coherence assessment. */
178
+ export interface KernelCoherenceVerdict {
179
+ readonly ok: boolean;
180
+ /** The required features the target kernel does NOT advertise. Empty ⇒ coherent. */
181
+ readonly missing: KernelFeature[];
182
+ /** A human-legible one-line message — the thing surfaced INSTEAD of the deep fold refusal. */
183
+ readonly message: string;
184
+ }
185
+
186
+ /** ASSESS whether a target kernel can fold a package's law. `requires` is the package's
187
+ * derived requirement ({@link requiredKernelFeatures}); `kernel` is the target's stamp
188
+ * (from its stamp op, or {@link capabilitiesForKernelHash}). A `null` kernel stamp means
189
+ * the target's capabilities are UNKNOWN — reported distinctly (can't prove coherence).
190
+ *
191
+ * The message names BOTH hashes and the offending capability — the co2 skew, legibly. */
192
+ export function assessKernelCoherence(params: {
193
+ requires: readonly KernelFeature[];
194
+ kernel: KernelStamp | null;
195
+ /** The target kernel's hash, for the message even when the stamp is unknown. */
196
+ kernelHash?: string;
197
+ /** The law's hash, for the message. */
198
+ lawHash?: string;
199
+ }): KernelCoherenceVerdict {
200
+ const { requires, kernel } = params;
201
+ const short = (h?: string) => (h ? `${String(h).replace(/^sha256:|^policy:/, "").slice(0, 8)}` : "<unknown>");
202
+ const lawH = short(params.lawHash);
203
+ const kernH = short(params.kernelHash ?? kernel?.hash);
204
+ if (requires.length === 0) {
205
+ return { ok: true, missing: [], message: `law ${lawH} requires no special kernel capabilities` };
206
+ }
207
+ if (kernel === null) {
208
+ return {
209
+ ok: false,
210
+ missing: [...requires],
211
+ message:
212
+ `cannot verify release coherence — this cloud's kernel ${kernH} is UNKNOWN to the ` +
213
+ `capability registry, so it cannot be confirmed to support [${requires.join(", ")}] that ` +
214
+ `law ${lawH} requires. Update the kernel registry or upgrade the cloud's kernel.`,
215
+ };
216
+ }
217
+ const missing = requires.filter((f) => !kernel.features.includes(f)) as KernelFeature[];
218
+ if (missing.length === 0) {
219
+ return { ok: true, missing: [], message: `kernel ${kernH} supports all capabilities law ${lawH} requires` };
220
+ }
221
+ return {
222
+ ok: false,
223
+ missing,
224
+ message:
225
+ `this cloud's kernel ${kernH} lacks capability '${missing.join("', '")}' that law ${lawH} requires — ` +
226
+ `the cloud is running a STALE kernel (${kernel.note ?? kernel.version}). ` +
227
+ (missing.includes("compoundKeys")
228
+ ? `A compound-key query (\`.key(a, b, …)\`) will be refused deep in a law fold with ` +
229
+ `"query declares N index keys; the read router supports exactly one". `
230
+ : ``) +
231
+ `Upgrade the cloud's kernel (or point at a cloud whose kernel is coherent with this release).`,
232
+ };
233
+ }
234
+
235
+ /** A typed error thrown when a coherence assertion fails — carries the structured verdict so
236
+ * callers can render it or branch, instead of parsing a message. */
237
+ export class KernelCoherenceError extends Error {
238
+ readonly verdict: KernelCoherenceVerdict;
239
+ readonly code = "kernel-capability-skew";
240
+ constructor(verdict: KernelCoherenceVerdict) {
241
+ super(verdict.message);
242
+ this.name = "KernelCoherenceError";
243
+ this.verdict = verdict;
244
+ }
245
+ }