@githolon/dsl 0.108.3 → 0.108.6

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.
Files changed (44) hide show
  1. package/dart/CHANGELOG.md +10 -0
  2. package/dart/lib/src/application_framework_calls.dart +21 -0
  3. package/dart/lib/src/dev_cloud.dart +67 -0
  4. package/dart/lib/src/nms_test_environment.dart +1 -1
  5. package/dart/lib/src/runner/node-entry.mjs +372 -106
  6. package/dart/lib/src/runner/offline-install.mjs +171 -11
  7. package/dart/lib/src/session_bound.dart +58 -30
  8. package/dart/lib/src/test_harness.dart +60 -98
  9. package/dart/lib/src/webview_bridge.dart +212 -32
  10. package/dart/pubspec.lock +1 -1
  11. package/dart/pubspec.yaml +2 -2
  12. package/dart/test/node_runner_transport_test.dart +5 -5
  13. package/dart/test/webview_bridge_test.dart +6 -0
  14. package/dart_types/CHANGELOG.md +4 -0
  15. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +428 -2
  16. package/dart_types/pubspec.yaml +1 -1
  17. package/package.json +1 -1
  18. package/runtime/generated-application-host.js +314 -82
  19. package/src/application_topology.ts +120 -28
  20. package/src/births.ts +27 -0
  21. package/src/codegen_call_usda.ts +9 -4
  22. package/src/codegen_dart_application.ts +146 -37
  23. package/src/codegen_dart_client.ts +225 -160
  24. package/src/codegen_dart_framework_calls.ts +89 -4
  25. package/src/codegen_ts.ts +135 -60
  26. package/src/compile_package_main.ts +3544 -1460
  27. package/src/directive.ts +320 -118
  28. package/src/fields.ts +2 -3
  29. package/src/framework/application_home.ts +3 -0
  30. package/src/framework/auth_provider_law.ts +20 -16
  31. package/src/framework/genesis.ts +32 -1
  32. package/src/framework/home.ts +45 -8
  33. package/src/framework/signing_identity.ts +78 -0
  34. package/src/framework/workspaces.ts +9 -1
  35. package/src/framework_call_dart_enums.ts +33 -0
  36. package/src/framework_call_definitions.ts +11 -2
  37. package/src/index.ts +12 -0
  38. package/src/intent.ts +525 -194
  39. package/src/law_graph.ts +32 -4
  40. package/src/law_graph_usda.ts +3 -0
  41. package/src/package_config.ts +16 -1
  42. package/src/read.ts +33 -11
  43. package/src/wire.ts +1 -0
  44. package/src/wire_encode.ts +3 -3
@@ -4390,6 +4390,8 @@ __export(engine_exports, {
4390
4390
  author: () => author,
4391
4391
  authorEphemeral: () => authorEphemeral,
4392
4392
  authorKeygen: () => authorKeygen,
4393
+ authorModelLifecyclePromotion: () => authorModelLifecyclePromotion,
4394
+ authorModelLifecycleStage: () => authorModelLifecycleStage,
4393
4395
  authorOn: () => authorOn,
4394
4396
  authorOperatorCall: () => authorOperatorCall,
4395
4397
  backupHead: () => backupHead,
@@ -4462,6 +4464,7 @@ __export(engine_exports, {
4462
4464
  lawParity: () => lawParity,
4463
4465
  ledgerStats: () => ledgerStats,
4464
4466
  listRefusedRefs: () => listRefusedRefs,
4467
+ localCustodySnapshot: () => localCustodySnapshot,
4465
4468
  materializeRefused: () => materializeRefused,
4466
4469
  memoryDropProbe: () => memoryDropProbe,
4467
4470
  migrateHomes: () => migrateHomes,
@@ -6274,6 +6277,16 @@ function prepareManagedPromotion(report, candidateHash, domain, opts = {}) {
6274
6277
  error: `the package-wide report does not cover the generated business area '${domain}'`
6275
6278
  };
6276
6279
  }
6280
+ const dispositions = report.dispositions;
6281
+ const changesStableIdentity = dispositions !== void 0 && (dispositions.retired.length > 0 || Object.keys(dispositions.retyped).length > 0 || Object.keys(dispositions.rebinds).length > 0);
6282
+ if (changesStableIdentity && !acceptanceReason) {
6283
+ return {
6284
+ ok: false,
6285
+ stage: "acceptance",
6286
+ report,
6287
+ error: "the current-frontier transition changes stable business identities and needs an attributed acceptance reason"
6288
+ };
6289
+ }
6277
6290
  const preflight = {
6278
6291
  candidateHash,
6279
6292
  // A package may be reached through any one of its generated domain clients. The
@@ -10898,6 +10911,7 @@ var AuthoringOptionsSchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_ke
10898
10911
  var CapturedClockSchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 49);
10899
10912
  var BirthCertificateSchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 50);
10900
10913
  var SignBirthCertificateRequestSchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 51);
10914
+ var ModelLifecycleIntentSchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 53);
10901
10915
  var OfferRequestSchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 54);
10902
10916
  var QueryRequestSchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 57);
10903
10917
  var QueryAuthoritySchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 64);
@@ -10912,6 +10926,10 @@ var PendingConflictsQuerySchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_
10912
10926
  var PendingConflictSummaryQuerySchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 77);
10913
10927
  var NamedQuerySchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 78);
10914
10928
  var FrontierTransitionRequestSchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 79);
10929
+ var FrontierTransitionDispositionMapEntrySchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 80);
10930
+ var FrontierTransitionDispositionsSchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 81);
10931
+ var FrontierTransitionViolationSchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 82);
10932
+ var FrontierTransitionResponseSchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 83);
10915
10933
  var AuthorizationObjectSchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 84);
10916
10934
  var AuthorizationSubjectSchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 85);
10917
10935
  var AuthorizationConsistencySchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 87);
@@ -10950,6 +10968,8 @@ var NomosValueSchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel,
10950
10968
  var NomosStructSchema = /* @__PURE__ */ messageDesc(file_nomos_kernel_v1_kernel, 166);
10951
10969
  var BirthOutcomeStatusSchema = /* @__PURE__ */ enumDesc(file_nomos_kernel_v1_kernel, 0);
10952
10970
  var BirthOutcomeStatus = /* @__PURE__ */ tsEnum(BirthOutcomeStatusSchema);
10971
+ var ModelLifecycleOperationSchema = /* @__PURE__ */ enumDesc(file_nomos_kernel_v1_kernel, 3);
10972
+ var ModelLifecycleOperation = /* @__PURE__ */ tsEnum(ModelLifecycleOperationSchema);
10953
10973
  var QueryResultFormatSchema = /* @__PURE__ */ enumDesc(file_nomos_kernel_v1_kernel, 5);
10954
10974
  var QueryResultFormat = /* @__PURE__ */ tsEnum(QueryResultFormatSchema);
10955
10975
  var QueryOperationSchema = /* @__PURE__ */ enumDesc(file_nomos_kernel_v1_kernel, 6);
@@ -11036,6 +11056,10 @@ function presentOfferAdmitted(admitted) {
11036
11056
  };
11037
11057
  }
11038
11058
 
11059
+ // src/_adapter/proto/nomos/custody/v1/snapshot_pb.js
11060
+ var file_nomos_custody_v1_snapshot = /* @__PURE__ */ fileDesc("Ch9ub21vcy9jdXN0b2R5L3YxL3NuYXBzaG90LnByb3RvEhBub21vcy5jdXN0b2R5LnYxIqACChRMb2NhbEN1c3RvZHlTbmFwc2hvdBIWCg5mb3JtYXRfdmVyc2lvbhgBIAEoDRIWCgl3b3Jrc3BhY2UYAiABKAlIAIgBARIMCgR0cmVlGAMgASgMEhcKCmNoZWNrcG9pbnQYBCABKAxIAYgBARIVCghmcm9udGllchgFIAEoDEgCiAEBEhMKBmN1cnNvchgGIAEoCUgDiAEBEhcKCnN0YXRlX3Jvb3QYByABKAlIBIgBARIYCgtzeW5jZWRfYmFzZRgIIAEoCUgFiAEBQgwKCl93b3Jrc3BhY2VCDQoLX2NoZWNrcG9pbnRCCwoJX2Zyb250aWVyQgkKB19jdXJzb3JCDQoLX3N0YXRlX3Jvb3RCDgoMX3N5bmNlZF9iYXNlYgZwcm90bzM");
11061
+ var LocalCustodySnapshotSchema = /* @__PURE__ */ messageDesc(file_nomos_custody_v1_snapshot, 0);
11062
+
11039
11063
  // src/_adapter/engine.ts
11040
11064
  var enc3 = new TextEncoder();
11041
11065
  var dec3 = new TextDecoder();
@@ -11586,6 +11610,100 @@ var closureInstallPayload = (hash, installedBy, options = {}) => ({
11586
11610
  function authorOperatorCall(eng2, ws, call, controllerHash, opts = {}) {
11587
11611
  return author(eng2, ws, call.domain, call.directive, call.canonicalUsda, controllerHash, opts);
11588
11612
  }
11613
+ function authorModelLifecycleStage(eng2, ws, lawUsdz, installedBy, supersedes, opts = {}) {
11614
+ const sequence = eng2.seq++;
11615
+ const captured = {
11616
+ physical: Date.now(),
11617
+ logical: 0,
11618
+ replica: eng2.replica
11619
+ };
11620
+ const response = offerRequestResponse(eng2, create(OfferRequestSchema, {
11621
+ target: protoTarget(ws),
11622
+ intent: {
11623
+ case: "modelLifecycle",
11624
+ value: create(ModelLifecycleIntentSchema, {
11625
+ operation: ModelLifecycleOperation.STAGE,
11626
+ installedBy,
11627
+ supersedes
11628
+ })
11629
+ },
11630
+ authoring: create(AuthoringOptionsSchema, {
11631
+ actor: opts.actor ?? "",
11632
+ authToken: opts.authToken ? enc3.encode(opts.authToken) : new Uint8Array(),
11633
+ authorSecret: opts.authorSecret ? bytesFromB64(opts.authorSecret) : new Uint8Array(),
11634
+ validFromEpoch: opts.validFromEpoch === void 0 ? void 0 : BigInt(opts.validFromEpoch),
11635
+ rng: osEntropyBuffer(opts.rngDraws ?? 64),
11636
+ deferProjection: opts.deferProjection !== false,
11637
+ spans: opts.spans ?? false,
11638
+ sequence: BigInt(sequence),
11639
+ clock: create(CapturedClockSchema, {
11640
+ physicalMillis: BigInt(captured.physical),
11641
+ logical: captured.logical,
11642
+ replica: BigInt(captured.replica)
11643
+ })
11644
+ }),
11645
+ lawUsdz
11646
+ }));
11647
+ return offerResult(response);
11648
+ }
11649
+ function authorModelLifecyclePromotion(eng2, ws, transition, replaces, acceptanceReason, opts = {}) {
11650
+ const sequence = eng2.seq++;
11651
+ const captured = {
11652
+ physical: Date.now(),
11653
+ logical: 0,
11654
+ replica: eng2.replica
11655
+ };
11656
+ const dispositions = transition.dispositions === void 0 ? void 0 : create(FrontierTransitionDispositionsSchema, {
11657
+ retired: [...transition.dispositions.retired],
11658
+ retyped: Object.entries(transition.dispositions.retyped).map(
11659
+ ([sourceStableId, acknowledgement]) => create(FrontierTransitionDispositionMapEntrySchema, { sourceStableId, acknowledgement })
11660
+ ),
11661
+ rebinds: Object.entries(transition.dispositions.rebinds).map(
11662
+ ([sourceStableId, acknowledgement]) => create(FrontierTransitionDispositionMapEntrySchema, { sourceStableId, acknowledgement })
11663
+ ),
11664
+ acknowledgedBy: transition.dispositions.ackBy
11665
+ });
11666
+ const response = offerRequestResponse(eng2, create(OfferRequestSchema, {
11667
+ target: protoTarget(ws),
11668
+ intent: {
11669
+ case: "modelLifecycle",
11670
+ value: create(ModelLifecycleIntentSchema, {
11671
+ operation: ModelLifecycleOperation.PROMOTE,
11672
+ replaces,
11673
+ acceptanceReason,
11674
+ transition: create(FrontierTransitionResponseSchema, {
11675
+ atHash: transition.atHash,
11676
+ currentHash: transition.currentHash,
11677
+ atStateRoot: transition.atStateRoot ?? "",
11678
+ candidateHash: transition.candidateHash,
11679
+ domain: transition.domain,
11680
+ domains: [...transition.domains ?? []],
11681
+ ready: transition.ready,
11682
+ violations: transition.evolve.violations.map(
11683
+ ({ domain, detail }) => create(FrontierTransitionViolationSchema, { domain, detail })
11684
+ ),
11685
+ dispositions
11686
+ })
11687
+ })
11688
+ },
11689
+ authoring: create(AuthoringOptionsSchema, {
11690
+ actor: opts.actor ?? "",
11691
+ authToken: opts.authToken ? enc3.encode(opts.authToken) : new Uint8Array(),
11692
+ authorSecret: opts.authorSecret ? bytesFromB64(opts.authorSecret) : new Uint8Array(),
11693
+ validFromEpoch: opts.validFromEpoch === void 0 ? void 0 : BigInt(opts.validFromEpoch),
11694
+ rng: osEntropyBuffer(opts.rngDraws ?? 64),
11695
+ deferProjection: opts.deferProjection !== false,
11696
+ spans: opts.spans ?? false,
11697
+ sequence: BigInt(sequence),
11698
+ clock: create(CapturedClockSchema, {
11699
+ physicalMillis: BigInt(captured.physical),
11700
+ logical: captured.logical,
11701
+ replica: BigInt(captured.replica)
11702
+ })
11703
+ })
11704
+ }));
11705
+ return offerResult(response);
11706
+ }
11589
11707
  function initializeWorkspace(eng2, ws, installedBy = eng2.installedBy) {
11590
11708
  const sequence = eng2.seq++;
11591
11709
  const captured = {
@@ -12704,8 +12822,11 @@ async function reconcileRequiredCustodyHead(eng2, ws, ledger, requiredCustodyHea
12704
12822
  if (!/^[0-9a-f]{40}$/.test(requiredCustodyHead)) {
12705
12823
  throw new Error("required custody head must be a lowercase 40-hex git object id");
12706
12824
  }
12707
- const current = eng2.mounted.has(ws) ? kgit.resolveRef(eng2, gitdirOf(ws), BRANCH) : "";
12708
- if (current === requiredCustodyHead) return eng2.mounted.get(ws);
12825
+ const resident = eng2.mounted.get(ws);
12826
+ const current = resident ? kgit.resolveRef(eng2, gitdirOf(ws), BRANCH) : "";
12827
+ if (current === requiredCustodyHead && resident?.remoteMain === requiredCustodyHead && !resident.restoreError) {
12828
+ return resident;
12829
+ }
12709
12830
  if (current && isDirty(eng2, ws)) {
12710
12831
  throw new Error(
12711
12832
  `workspace '${ws}' has verified but unsealed resident work; refusing to discard it while reconciling custody`
@@ -12718,6 +12839,12 @@ async function reconcileRequiredCustodyHead(eng2, ws, ledger, requiredCustodyHea
12718
12839
  `workspace '${ws}' could not reconcile required custody ${requiredCustodyHead}: ${mounted.restoreError || "custody has no verified main"}`
12719
12840
  );
12720
12841
  }
12842
+ const reconciled = kgit.resolveRef(eng2, gitdirOf(ws), BRANCH);
12843
+ if (reconciled !== requiredCustodyHead || mounted.remoteMain !== requiredCustodyHead) {
12844
+ throw new Error(
12845
+ `workspace '${ws}' restored custody ${mounted.remoteMain || reconciled || "none"}, not required custody ${requiredCustodyHead}`
12846
+ );
12847
+ }
12721
12848
  return mounted;
12722
12849
  }
12723
12850
  function assertAuthored(res, what) {
@@ -12855,7 +12982,10 @@ async function applyWorkspaceEffect(eng2, ws, ledger, requestBytes, admissionChe
12855
12982
  const result = response.outcome.value.result;
12856
12983
  if (!result || result.outcome.case !== "admitted") {
12857
12984
  const outcome = result?.outcome.case || "empty";
12858
- throw new Error(`target kernel ${outcome} the workspace effect`);
12985
+ const detail = result?.outcome.case === "refused" ? result.outcome.value.refusal?.message : void 0;
12986
+ throw new Error(
12987
+ `target kernel ${outcome} the workspace effect${detail ? `: ${detail}` : ""}`
12988
+ );
12859
12989
  }
12860
12990
  await custodyPush(eng2, ws, ledger, { remoteRef: "refs/heads/main", ffOnly: true });
12861
12991
  return toBinary(ApplyWorkspaceEffectResponseSchema, response.outcome.value);
@@ -12966,8 +13096,8 @@ function prepareRemoteOffer(eng2, ws, requestBytes) {
12966
13096
  if (authoring.authorSecret.byteLength !== 0) {
12967
13097
  throw new Error("remote offer preparation refuses client signing secrets");
12968
13098
  }
12969
- if (request.intent.case !== "call") {
12970
- throw new Error("remote offer preparation requires a typed directive call");
13099
+ if (request.intent.case !== "call" && request.intent.case !== "modelLifecycle") {
13100
+ throw new Error("remote offer preparation requires a typed directive or model-lifecycle call");
12971
13101
  }
12972
13102
  const response = callProto(eng2.ex, {
12973
13103
  case: "prepareRemoteOffer",
@@ -13774,7 +13904,13 @@ function packDelta(eng2, ws, base, head) {
13774
13904
  case: "packDelta",
13775
13905
  value: create(PackDeltaRequestSchema, { repo: repoArgOf(ws), base: base || void 0, head })
13776
13906
  }, "packDelta").pack;
13777
- } catch {
13907
+ } catch (error) {
13908
+ console.error("[nomos-custody-pack] packDelta refused", {
13909
+ workspace: ws,
13910
+ base: base || "<full>",
13911
+ head,
13912
+ error: String(error)
13913
+ });
13778
13914
  return null;
13779
13915
  }
13780
13916
  }
@@ -14457,11 +14593,12 @@ function promotePreflighted(eng2, ws, domain, candidateHash, opts = {}) {
14457
14593
  acceptanceReason: _acceptance,
14458
14594
  ...authorOpts
14459
14595
  } = opts;
14460
- const result = authorOperatorCall(
14596
+ const result = authorModelLifecyclePromotion(
14461
14597
  eng2,
14462
14598
  ws,
14463
- promoteDomainCall(ws, prepared.payload),
14464
- requireCurrentControllerLawHash(eng2, ws),
14599
+ report,
14600
+ prepared.payload.replaces,
14601
+ prepared.payload.preflight.acceptanceReason,
14465
14602
  {
14466
14603
  ...authorOpts,
14467
14604
  ...actingPrincipal ? { actor: actingPrincipal } : {}
@@ -14952,7 +15089,10 @@ async function emitOutbox(eng2, ws, ledger, workspaceEffectBase = "", admissionC
14952
15089
  const targetResult = response.result;
14953
15090
  if (!targetResult || targetResult.outcome.case !== "admitted") {
14954
15091
  const outcome = targetResult?.outcome.case || "empty";
14955
- throw new Error(`target kernel ${outcome} the workspace effect`);
15092
+ const detail = targetResult?.outcome.case === "refused" ? targetResult.outcome.value.refusal?.message : void 0;
15093
+ throw new Error(
15094
+ `target kernel ${outcome} the workspace effect${detail ? `: ${detail}` : ""}`
15095
+ );
14956
15096
  }
14957
15097
  break;
14958
15098
  }
@@ -14984,7 +15124,7 @@ async function emitOutbox(eng2, ws, ledger, workspaceEffectBase = "", admissionC
14984
15124
  } catch (e) {
14985
15125
  errors++;
14986
15126
  const destination = offer.route.case === "httpsRoute" ? offer.route.value.url : offer.route.case === "workspaceRoute" ? offer.route.value.workspace : offer.destination;
14987
- log("outbox-deliver-failed", { ws, offerId: offer.id, destination, error: String(e).slice(0, 200) });
15127
+ log("outbox-deliver-failed", { ws, offerId: offer.id, destination, error: String(e).slice(0, 2e3) });
14988
15128
  }
14989
15129
  }
14990
15130
  if (delivered.length || errors) log("outbox", { ws, delivered: delivered.length, errors });
@@ -15017,6 +15157,26 @@ function snapshot(eng2, ws) {
15017
15157
  const wsDir = dirOf(eng2.preopen.dir.contents.get("ws")).contents.get(ws);
15018
15158
  return serializeTree(wsDir);
15019
15159
  }
15160
+ function localCustodySnapshot(eng2, ws) {
15161
+ const checkpoint2 = exportCheckpoint(eng2, ws);
15162
+ const tree = snapshot(eng2, ws);
15163
+ const head = headOf(eng2, ws);
15164
+ const body = toBinary(LocalCustodySnapshotSchema, create(LocalCustodySnapshotSchema, {
15165
+ formatVersion: 3,
15166
+ workspace: ws,
15167
+ tree,
15168
+ checkpoint: checkpoint2.blob ? bytesFromB64(checkpoint2.blob) : void 0,
15169
+ frontier: checkpoint2.frontier ? bytesFromB64(checkpoint2.frontier) : void 0,
15170
+ cursor: checkpoint2.cursor === void 0 ? void 0 : String(checkpoint2.cursor),
15171
+ stateRoot: checkpoint2.stateRoot,
15172
+ syncedBase: head || void 0
15173
+ }));
15174
+ const prefix = enc3.encode("NOMOS-CUSTODY-PB3\n");
15175
+ const framed = new Uint8Array(prefix.byteLength + body.byteLength);
15176
+ framed.set(prefix);
15177
+ framed.set(body, prefix.byteLength);
15178
+ return framed;
15179
+ }
15020
15180
  function stats(eng2) {
15021
15181
  return {
15022
15182
  mounted: eng2.mounted.size,
@@ -284,6 +284,10 @@ Future<T> nmsInternalConvergeGeneratedModel<T>({
284
284
  try {
285
285
  return await attempt();
286
286
  } on DomainClientModelAdoptionRequired catch (change) {
287
+ // A lifecycle response with no lawful next action is a terminal refusal,
288
+ // not a convergence step. Preserve the kernel's exact diagnostic instead
289
+ // of retrying until a generic stall obscures it.
290
+ if (!change.canStage && !change.canAccept) throw change;
287
291
  final step = '${change.expectedDomainKey}\u0000'
288
292
  '${change.expectedDomainHash}\u0000'
289
293
  '${change.status}\u0000'
@@ -300,10 +304,11 @@ Future<T> nmsInternalConvergeGeneratedModel<T>({
300
304
  }
301
305
  try {
302
306
  await handle(change);
303
- } on DomainClientModelAdoptionRequired {
307
+ } on DomainClientModelAdoptionRequired catch (nextChange) {
304
308
  // A transient preflight/acceptance race re-enters the same typed step.
305
309
  // The per-step budget above prevents a genuine loop without limiting
306
310
  // how many distinct historical generations may converge.
311
+ if (!nextChange.canStage && !nextChange.canAccept) throw nextChange;
307
312
  }
308
313
  }
309
314
  }
@@ -379,35 +384,56 @@ class DomainNotInstalledForSession implements Exception {
379
384
  /// cause classified; returns normally when the session serves the client's law compatibly.
380
385
  extension NomosSessionCompat on NomosSession {
381
386
  Future<void> assertCompatible(NomosClientInterface iface) async {
382
- // 1. REACHABILITY — the session's workspace must answer a read at all.
387
+ // The exact generated package identity is stronger than a structural comparison. The kernel's
388
+ // point-readable `law/current.usda` answers this bounded happy path without materialising the
389
+ // complete interface offer. Structural discovery is reserved for genuine version skew.
383
390
  List<String> activeKeys = const [];
391
+ List<String> activeHashes = const [];
384
392
  try {
385
- final offer = await interfaceOffer();
386
- if (offer != null) activeKeys = offer.keys.toList()..sort();
393
+ final law = await lawStatus(iface.expectedDomainHash);
394
+ activeHashes = ((law['current'] as List?) ?? const [])
395
+ .map((value) => value.toString())
396
+ .toList(growable: false);
397
+ if (activeHashes.contains(iface.expectedDomainHash)) return;
398
+ activeKeys = ((law['currentDomains'] as List?) ?? const [])
399
+ .map((value) => value.toString())
400
+ .toList(growable: false);
401
+ // Compatibility is member-scoped, not one enormous application-wide ceremony. Once the
402
+ // authenticated current-law index proves this domain is served, generated reads decode their
403
+ // exact USDA result types and generated offers are judged by the current kernel route. A new
404
+ // member therefore fails closed when used without blocking every older compatible feature.
405
+ if (activeKeys.contains(iface.domain)) return;
406
+ } catch (_) {
407
+ // Preserve the structural fallback for older kernels which do not expose the typed current
408
+ // frontier. Its error classification below remains the authoritative diagnostic path.
409
+ }
410
+
411
+ // 1. THE SHAPE AXIS (#72) — the one compatibility notion. The kernel compares the
412
+ // compact generated requirement directly with its authenticated current-law index. Do not
413
+ // export/materialise the complete interface merely to prove the successful path.
414
+ final requirement = iface.requirement;
415
+ if (requirement == null) return;
416
+ Map<String, Object?> verdict;
417
+ try {
418
+ final raw = await checkCompatibility(requirement);
419
+ verdict = (raw['verdict'] is Map)
420
+ ? (raw['verdict'] as Map).cast<String, Object?>()
421
+ : raw;
387
422
  } catch (e) {
388
- // co2 blocker #2: a PROJECTION READ FAILURE (an unmountable/failing query shape — the runner surfaces
389
- // `nomos.projection.read_failed` carrying the failed query) is NOT "not reachable" and MUST NOT be
390
- // reclassified as trulyNotDeployed downstream — surface it as its own cause with the real fault text.
391
- final isProjectionFault =
392
- e.toString().contains('nomos.projection.read_failed') ||
393
- e.toString().contains('projection read failed');
394
423
  throw DomainClientSessionMismatch(
395
424
  expectedDomainKey: iface.domain,
396
425
  expectedDomainHash: iface.expectedDomainHash,
397
426
  expectedInterfaceHash: iface.expectedInterfaceHash,
398
427
  sessionId: workspace,
399
428
  sessionWorkspace: workspace,
400
- cause: isProjectionFault
401
- ? SessionMismatchCause.projectionReadFailed
402
- : null,
403
- hint: isProjectionFault
404
- ? 'the workspace law could NOT be READ — a projection/mount failure (NOT undeployed law): $e'
405
- : 'the session workspace is not reachable/mounted on this plane: $e',
429
+ activeDomainKeys: activeKeys,
430
+ activeDomainHashes: activeHashes,
431
+ hint: 'the shape-compatibility check itself failed: $e',
406
432
  );
407
433
  }
434
+ if (verdict['ok'] == true) return;
408
435
 
409
- // 2. HASH AXIS (diagnosis) — the installed law hashes, via the baked compat manifest when present.
410
- List<String> activeHashes = const [];
436
+ // 2. HASH DIAGNOSIS — only a failed verdict needs retained installation inventory.
411
437
  if (iface.compatJson != null && iface.compatJson!.isNotEmpty) {
412
438
  try {
413
439
  final c = await checkCompat(
@@ -415,32 +441,34 @@ extension NomosSessionCompat on NomosSession {
415
441
  iface.requirement);
416
442
  if (c != null) activeHashes = c.workspaceRuns;
417
443
  } catch (_) {
418
- /* diagnosis only — never fail the preflight on the hash axis */
444
+ /* diagnosis only — never replace the shape verdict */
419
445
  }
420
446
  }
421
447
 
422
- // 3. THE SHAPE AXIS (#72) — the one compatibility notion. No baked interface ⇒ nothing more to check.
423
- final requirement = iface.requirement;
424
- if (requirement == null) return;
425
- Map<String, Object?> verdict;
448
+ // 3. DIAGNOSTIC OFFER — only a failed verdict needs the complete active-domain names to
449
+ // distinguish a newer client from the wrong package or a genuinely absent deployment.
426
450
  try {
427
- final raw = await checkCompatibility(requirement);
428
- verdict = (raw['verdict'] is Map)
429
- ? (raw['verdict'] as Map).cast<String, Object?>()
430
- : raw;
451
+ final offer = await interfaceOffer();
452
+ if (offer != null) activeKeys = offer.keys.toList()..sort();
431
453
  } catch (e) {
454
+ final isProjectionFault =
455
+ e.toString().contains('nomos.projection.read_failed') ||
456
+ e.toString().contains('projection read failed');
432
457
  throw DomainClientSessionMismatch(
433
458
  expectedDomainKey: iface.domain,
434
459
  expectedDomainHash: iface.expectedDomainHash,
435
460
  expectedInterfaceHash: iface.expectedInterfaceHash,
436
461
  sessionId: workspace,
437
462
  sessionWorkspace: workspace,
438
- activeDomainKeys: activeKeys,
439
463
  activeDomainHashes: activeHashes,
440
- hint: 'the shape-compatibility check itself failed: $e',
464
+ cause: isProjectionFault
465
+ ? SessionMismatchCause.projectionReadFailed
466
+ : null,
467
+ hint: isProjectionFault
468
+ ? 'the workspace law could NOT be READ — a projection/mount failure (NOT undeployed law): $e'
469
+ : 'the session workspace is not reachable/mounted on this plane: $e',
441
470
  );
442
471
  }
443
- if (verdict['ok'] == true) return;
444
472
 
445
473
  final missing =
446
474
  ((verdict['missing'] as List?) ?? const []).map((e) => '$e').toList();
@@ -43,7 +43,7 @@ import 'dart:io';
43
43
  import 'package:crypto/crypto.dart' show sha256;
44
44
  import 'package:fixnum/fixnum.dart' show Int64;
45
45
  import 'package:nomos_types/nomos_kernel_protocol.dart'
46
- show BirthCertificate, BirthOutcomeStatus, SyncWorkspaceResponse;
46
+ show BirthCertificate, SyncWorkspaceResponse;
47
47
  import 'package:nomos_types/nomos_types.dart' show Sha256Digest;
48
48
  import 'package:package_config/package_config.dart' show loadPackageConfigUri;
49
49
 
@@ -494,6 +494,14 @@ class NomosTestHarness {
494
494
  /// `sha256(base64decode(...))` is the attestor keyHash a reader enrols to trust this source's signed reads.
495
495
  String? devicePublicKey(String workspace) => _devicePublics[workspace];
496
496
 
497
+ /// The enrolled device identity used by provider nonce binding. Tests which
498
+ /// rotate an external IdP can mint the next proof without handling key bytes.
499
+ String? deviceKeyHash(String workspace) {
500
+ final publicKey = _devicePublics[workspace];
501
+ if (publicKey == null || publicKey.isEmpty) return null;
502
+ return sha256.convert(base64Decode(publicKey)).toString();
503
+ }
504
+
497
505
  /// The base64 device SECRET a session was opened with (TEST HARNESS ONLY — the harness IS the device;
498
506
  /// a real app keeps this in secure storage). Sign writes to locally-born children with it, e.g.
499
507
  /// `bridge.acquireSession(childWs, authorSecret: harness.deviceSecret(home))`.
@@ -1480,6 +1488,7 @@ class NomosTestHarness {
1480
1488
  static Future<NomosTestHarness> cloudBound({
1481
1489
  required NomosTestCloud cloud,
1482
1490
  required List<String> lawUsdzPaths,
1491
+ NmsApplicationFrameworkCalls? applicationFrameworkCalls,
1483
1492
  String platformWorkspace = 'test-platform',
1484
1493
  required String subject,
1485
1494
  String? authToken,
@@ -1500,6 +1509,13 @@ class NomosTestHarness {
1500
1509
  'no lawUsdzPaths — pass the build/<name>.law.usdz `npx githolon compile` emits',
1501
1510
  );
1502
1511
  }
1512
+ final frameworkCalls = applicationFrameworkCalls;
1513
+ if (frameworkCalls == null) {
1514
+ throw NomosTestHarnessException(
1515
+ 'cloudBound requires this application\'s compiler-generated '
1516
+ 'NmsApplicationFrameworkCalls; raw framework directive transport is not supported',
1517
+ );
1518
+ }
1503
1519
  final entry = nodeEntry ??
1504
1520
  await _resolveEntry(node, resolveFrom ?? Directory.current.path);
1505
1521
  final bareUid =
@@ -1853,56 +1869,18 @@ class NomosTestHarness {
1853
1869
  'capturedClock': DateTime.now().toUtc().millisecondsSinceEpoch,
1854
1870
  'epoch': 0,
1855
1871
  }, secret);
1856
- // Mount the target workspace in the SAME kernel plane and author through its own
1857
- // installed law. The HTTP host is neither given raw business fields nor a signing key;
1858
- // sync carries the already-sealed offer and the receiving kernel replays the one gate.
1859
- // The enroll route was still resolved above to prove the generated public surface
1860
- // names this semantic route exactly once, but route strings grant no authority here.
1861
- final platformAuthorSession = '$clientId-platform-author';
1862
- await plane.connect(
1863
- cloud: cloud.origin,
1864
- workspace: platformWorkspace,
1865
- session: platformAuthorSession,
1866
- clientId: '$clientId-platform-author',
1867
- authToken: idpToken,
1868
- authorSecret: secret,
1869
- );
1870
- await plane.offerDirective(
1871
- domain: applicationHomeDomain,
1872
- directiveId: 'enrollFirstDevice',
1873
- domainHash: controlHomeLawHash,
1874
- actor: subject,
1875
- authorSecret: secret,
1876
- session: platformAuthorSession,
1877
- payload: {
1878
- 'keyHash': keyHash,
1879
- 'verifiedVia': issuer,
1880
- 'validFromEpoch': 0,
1881
- 'grantedAt': DateTime.now().toUtc().toIso8601String(),
1882
- 'idpToken': idpToken,
1883
- // The framework law owns this USDA object shape. Keep the
1884
- // compiler-generated protobuf certificate structured while
1885
- // lowering it into that domain value; never resurrect the
1886
- // retired JSON string envelope (`delegationCert`).
1887
- 'delegationCertificate':
1888
- _birthCertificateLawValue(delegationCert),
1889
- },
1890
- );
1891
- await plane.sync(
1892
- session: platformAuthorSession,
1893
- );
1894
1872
  return (
1895
1873
  publicB64: publicB64,
1896
1874
  secret: secret,
1897
1875
  keyHash: keyHash,
1898
1876
  idpToken: idpToken,
1877
+ issuer: issuer,
1878
+ delegationCert: delegationCert,
1899
1879
  );
1900
1880
  },
1901
1881
  );
1902
- // 5/6 — THE HOME CEREMONY. Author the birth in the already-mounted PARENT kernel, then sync its sealed
1903
- // intent to the remote parent where the same offer gate replays it. The HTTP host never receives
1904
- // business fields or a signing secret and has no birth-authoring lane. The offer carries the
1905
- // typed device-signed certificate. The kernel derives and enrolls the founding signer from it.
1882
+ // 5/6 — THE PRODUCTION HOME CEREMONY. Platform remains remote: Runtime v2 prepares both generated
1883
+ // calls against its live frontier, signs with this device, and opens the resulting Home custody.
1906
1884
  String? home;
1907
1885
  await _phased(
1908
1886
  phase: '5/6: remote home-birth ceremony (attested)',
@@ -1912,65 +1890,49 @@ class NomosTestHarness {
1912
1890
  onDiagnostic: onDiagnostic,
1913
1891
  run: () async {
1914
1892
  final homeName = _receivingGroupNameOf(subject);
1915
- // The plane deliberately still has both the empty onboarding scratch holon and the
1916
- // authenticated platform holon mounted. Bind this parent certificate operation to the
1917
- // platform session explicitly; relying on the single-session convenience API becomes
1918
- // ambiguous as soon as the ceremony correctly opens the target workspace in-kernel.
1919
- final homeCert =
1920
- await plane.session('$clientId-platform-author').signBirthCert(
1921
- BirthCertificate(
1922
- child: homeName,
1923
- founder: subject,
1924
- // The cert's `verified` must MATCH the gate verdict (CAS-as-law): the offer carries
1925
- // the dev-IdP token, so the gate resolves a VERIFIED founder.
1926
- verified: true,
1927
- parentKey: base64Decode(device.publicB64),
1928
- parentId: platformWorkspace,
1929
- epoch: Int64.ZERO,
1930
- ),
1931
- device.secret,
1932
- );
1933
- final localBirth =
1934
- await plane.session('$clientId-platform-author').offerOutcome(
1935
- domain: applicationHomeDomain,
1936
- directiveId: 'nomosInternalBirthReceivingGroup7c1f5e',
1937
- actor: subject,
1938
- authorSecret: device.secret,
1939
- payload: <String, Object?>{
1940
- 'owner': subject,
1941
- 'frameworkHash': frameworkHash,
1942
- 'lawHash': applicationHomeLawHash,
1943
- 'bornAt': DateTime.now().toUtc().toIso8601String(),
1944
- },
1945
- birthCertificate: homeCert,
1946
- );
1947
- if (!localBirth.born.contains(homeName)) {
1948
- throw NomosTestHarnessException(
1949
- 'cloudBound: the local parent kernel admitted the receiving-group offer but did not birth '
1950
- '"$homeName" (born=${localBirth.born}, birthOutcomes=${localBirth.birthOutcomes})',
1951
- );
1952
- }
1953
- final birthSync = await plane.sync(
1954
- session: '$clientId-platform-author',
1955
- );
1956
- if (birthSync.pushFailed ||
1957
- birthSync.diverged ||
1958
- birthSync.stillAhead) {
1893
+ final bornAt = DateTime.now().toUtc().toIso8601String();
1894
+ final result = await plane
1895
+ .session('$clientId-onboard-scratch')
1896
+ .nomosInternalBirthReceivingGroup(
1897
+ parent: platformWorkspace,
1898
+ principal: bareUid,
1899
+ subject: subject,
1900
+ device: {
1901
+ 'public': device.publicB64,
1902
+ 'secret': device.secret,
1903
+ },
1904
+ authToken: device.idpToken,
1905
+ idToken: device.idpToken,
1906
+ verifiedVia: device.issuer,
1907
+ bornAt: bornAt,
1908
+ enrollFirstDeviceCall: frameworkCalls.enrollFirstDevice(
1909
+ domain: applicationHomeDomain,
1910
+ workspace: platformWorkspace,
1911
+ keyHash: device.keyHash,
1912
+ verifiedVia: device.issuer,
1913
+ validFromEpoch: 0,
1914
+ grantedAt: bornAt,
1915
+ idpToken: device.idpToken,
1916
+ delegationCertificate: device.delegationCert,
1917
+ ),
1918
+ birthReceivingGroupCall: frameworkCalls.birthReceivingGroup(
1919
+ domain: applicationHomeDomain,
1920
+ workspace: platformWorkspace,
1921
+ owner: subject,
1922
+ frameworkHash: Sha256Digest(frameworkHash!),
1923
+ lawHash: Sha256Digest(applicationHomeLawHash),
1924
+ bornAt: bornAt,
1925
+ ),
1926
+ confirm: true,
1927
+ );
1928
+ if (result.home != homeName) {
1959
1929
  throw NomosTestHarnessException(
1960
- 'cloudBound: the sealed receiving-group offer did not converge: $birthSync',
1930
+ 'cloudBound: production custody ceremony returned ${result.home} instead of "$homeName"',
1961
1931
  );
1962
1932
  }
1963
- final remoteOutcomes = birthSync.birthOutcomes;
1964
- final failedRemoteBirth = remoteOutcomes.where(
1965
- (entry) =>
1966
- entry.hasWorkspace() &&
1967
- entry.workspace == homeName &&
1968
- entry.status != BirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN,
1969
- );
1970
- if (failedRemoteBirth.isNotEmpty) {
1933
+ if (!result.verified) {
1971
1934
  throw NomosTestHarnessException(
1972
- 'cloudBound: the remote parent replay refused the receiving-group birth: '
1973
- '${failedRemoteBirth.toList()}',
1935
+ 'cloudBound: born Home failed kernel verification: ${result.verificationError}',
1974
1936
  );
1975
1937
  }
1976
1938
  home = homeName;