@githolon/dsl 0.100.5 → 0.101.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.
Files changed (97) hide show
  1. package/dart/CHANGELOG.md +12 -0
  2. package/dart/lib/src/dev_cloud.dart +46 -15
  3. package/dart/lib/src/test_harness.dart +220 -71
  4. package/dart/lib/src/webview_bridge.dart +640 -126
  5. package/dart/pubspec.lock +1 -1
  6. package/dart/pubspec.yaml +2 -2
  7. package/dart/test/acquire_session_credentials_test.dart +1 -1
  8. package/dart/test/adv_realm_interference_test.dart +1 -1
  9. package/dart/test/bundle_version_pairing_test.dart +11 -22
  10. package/dart/test/cluster_attested_test.dart +1 -1
  11. package/dart/test/cluster_births_test.dart +1 -1
  12. package/dart/test/cluster_device_keyed_acceptance_test.dart +1 -1
  13. package/dart/test/cluster_local_autosync_test.dart +1 -1
  14. package/dart/test/cluster_scale_test.dart +1 -1
  15. package/dart/test/co2_product_gauntlet_test.dart +228 -0
  16. package/dart/test/dlq_session_surface_test.dart +1 -1
  17. package/dart/test/errors_test.dart +63 -12
  18. package/dart/test/first_app_gauntlet_test.dart +132 -87
  19. package/dart/test/local_sync_inert_test.dart +10 -5
  20. package/dart/test/parent_signer_recovery_test.dart +1 -1
  21. package/dart/test/sealing_client_test.dart +1 -1
  22. package/dart/test/session_bound_test.dart +1 -1
  23. package/dart/test/test_harness_test.dart +2 -2
  24. package/dart/test/vo_read_typing_test.dart +1 -1
  25. package/dart/test/watch_after_offer_stall_test.dart +1 -1
  26. package/dart/test/webview_bridge_test.dart +274 -0
  27. package/dart_types/CHANGELOG.md +6 -0
  28. package/dart_types/lib/src/errors.dart +136 -30
  29. package/dart_types/lib/src/surfaced.dart +48 -2
  30. package/dart_types/lib/src/wire.dart +3 -1
  31. package/dart_types/pubspec.yaml +1 -1
  32. package/dart_types/test/surfaced_test.dart +19 -0
  33. package/package.json +1 -1
  34. package/src/activity.ts +43 -4
  35. package/src/aggregate.ts +132 -22
  36. package/src/attested_core.ts +1 -1
  37. package/src/authoring.ts +119 -6
  38. package/src/birth_meter.ts +4 -4
  39. package/src/birth_meter_ensure.ts +165 -29
  40. package/src/births.ts +53 -22
  41. package/src/build_package.ts +96 -20
  42. package/src/business_birth.ts +79 -16
  43. package/src/codegen_dart.ts +267 -37
  44. package/src/codegen_dart_client.ts +930 -87
  45. package/src/codegen_openapi.ts +28 -5
  46. package/src/codegen_proof.ts +107 -11
  47. package/src/codegen_ts.ts +961 -135
  48. package/src/compile_package_main.ts +172 -53
  49. package/src/conflicts.ts +252 -0
  50. package/src/describe_model.ts +448 -0
  51. package/src/directive.ts +5 -38
  52. package/src/drivers.ts +12 -4
  53. package/src/engine_entry.ts +4 -0
  54. package/src/evolve_preview.ts +72 -11
  55. package/src/fields.ts +201 -55
  56. package/src/framework/application_home.ts +103 -0
  57. package/src/framework/application_home_domain.ts +2 -0
  58. package/src/framework/attested_receipts.ts +2 -2
  59. package/src/framework/auth_provider_law.ts +51 -24
  60. package/src/framework/business_corrections.ts +170 -0
  61. package/src/framework/disclosure.ts +1 -1
  62. package/src/framework/domain_lifecycle.ts +250 -16
  63. package/src/framework/encryption_identity.ts +1 -1
  64. package/src/framework/encryption_scope.ts +21 -13
  65. package/src/framework/era_certification.ts +1 -1
  66. package/src/framework/framework_aggregates.ts +2 -0
  67. package/src/framework/genesis.ts +1 -1
  68. package/src/framework/home.ts +22 -9
  69. package/src/framework/identity.ts +5 -5
  70. package/src/framework/impure_capability.ts +2 -2
  71. package/src/framework/owned_custody.ts +561 -0
  72. package/src/framework/platform_admin.ts +1 -1
  73. package/src/framework/relations.ts +9 -1
  74. package/src/framework/repair.ts +1 -1
  75. package/src/framework/signing_identity.ts +2 -2
  76. package/src/framework/source_roster.ts +1 -1
  77. package/src/framework/workspaces.ts +17 -10
  78. package/src/generated_client_names.ts +39 -0
  79. package/src/index.ts +20 -15
  80. package/src/kernel_capabilities.ts +25 -1
  81. package/src/law_check.ts +4 -1
  82. package/src/manifest.ts +356 -63
  83. package/src/read_projection_manifest.ts +8 -0
  84. package/src/read_projection_namespace.ts +4 -0
  85. package/src/relation.ts +1 -1
  86. package/src/shape_compat.ts +61 -5
  87. package/src/stable_ids.ts +38 -6
  88. package/src/stable_ids_types.ts +30 -0
  89. package/src/surfaced_lint.ts +0 -0
  90. package/src/usd.ts +62 -11
  91. package/src/usd_layers.ts +54 -3
  92. package/src/value_object.ts +23 -19
  93. package/src/wire.ts +1 -0
  94. package/src/wire_encode.ts +48 -4
  95. package/src/workspace_routing.ts +57 -0
  96. package/src/workspace_sharding.ts +9 -9
  97. package/src/certified_read.ts +0 -313
@@ -24,7 +24,7 @@
24
24
  // snapshot (`restoreFrom`). Every other URL is REFUSED — a stray fetch fails loudly, never reaches the net.
25
25
  //
26
26
  // PREREQUISITES (documented so a failure is legible): `node` on PATH; a resolvable
27
- // `@githolon/client/runner/node-entry.ts`; and a warm `~/.holon/runtime` cache (holon.wasm + packages.json —
27
+ // `@githolon/client/runner/node-entry.ts`; and a warm `~/.holon/runtime` cache (holon-testmint.wasm + packages.json —
28
28
  // one `githolon` run fills it). All local; no network at test time.
29
29
  //
30
30
  // PACKAGING NOTE: `offline-install.ts` imports the adapter engine, which today lives beside the in-repo
@@ -173,8 +173,8 @@ class NomosTestHarness {
173
173
  return session(ws);
174
174
  }
175
175
 
176
- NomosTestHarness._(this.bridge, this.workspace, this._transport, this._workdir,
177
- this._onDiagnostic, List<String> sessionNames,
176
+ NomosTestHarness._(this.bridge, this.workspace, this._transport,
177
+ this._workdir, this._onDiagnostic, List<String> sessionNames,
178
178
  {Map<String, String> devicePublics = const {},
179
179
  Map<String, String> deviceSecrets = const {},
180
180
  this.frameworkHash,
@@ -184,7 +184,8 @@ class NomosTestHarness {
184
184
  Duration? phaseTimeout,
185
185
  this.testCloud,
186
186
  this.homeWorkspace})
187
- : _sessionNames = List.of(sessionNames), // GROWABLE — restoreWorkspace may add a fresh session
187
+ : _sessionNames = List.of(
188
+ sessionNames), // GROWABLE — restoreWorkspace may add a fresh session
188
189
  _devicePublics = devicePublics,
189
190
  _deviceSecrets = deviceSecrets,
190
191
  _lawHashes = Map.of(lawHashes),
@@ -256,10 +257,11 @@ class NomosTestHarness {
256
257
  /// [deployJsonPaths] — the `build/<name>.deploy.json` files `nomos-compile` emits (installed in order;
257
258
  /// framework/platform law before a domain that depends on it).
258
259
  /// [workspace] — the local workspace name (default `test-ws`; law-only, never leaves the machine).
259
- /// [runtimeDir] — the runtime cache (default `~/.holon/runtime`); must hold `holon.wasm` + `packages.json`.
260
+ /// [runtimeDir] — the runtime cache (default `~/.holon/runtime`); must hold `holon-testmint.wasm` +
261
+ /// `packages.json`. The harness owns a throwaway test root; app code never handles its key material.
260
262
  /// [nodeEntry] — path to `node-entry.ts`; when omitted it is resolved from [resolveFrom] via
261
263
  /// `require.resolve('@githolon/client/runner/node-entry.ts')`.
262
- /// [installHelper] — the offline law-install lane. An EXISTING `.mjs` file is run as a node script (the
264
+ /// [installHelper] — the offline law-install lane. An EXISTING `.ts` or `.mjs` file is run as a node script (the
263
265
  /// in-repo `offline-install.ts`, the default when it exists beside [nodeEntry]); anything else is a
264
266
  /// githolon COMMAND LINE whose `harness-install` subcommand is driven (the default `npx githolon` when the
265
267
  /// in-repo helper is absent — a published tenant needs nothing but `githolon`). E.g. force the subcommand
@@ -291,16 +293,20 @@ class NomosTestHarness {
291
293
  final entry = nodeEntry ??
292
294
  await _resolveEntry(node, resolveFrom ?? Directory.current.path);
293
295
  // THE INSTALL LANE. [installHelper] is the one seam (PACKAGING NOTE above): either the in-repo
294
- // `offline-install.ts` node script, OR the published `githolon harness-install` subcommand (the CLI
296
+ // `offline-install.mjs` node script, OR the published `githolon harness-install` subcommand (the CLI
295
297
  // bundles the engine + this exact install sequence, so a published tenant needs nothing but `githolon`).
296
- // • an EXISTING `.mjs` file ⇒ run it as a node script (the in-repo default when present);
298
+ // • an EXISTING `.ts`/`.mjs` file ⇒ run it as a node script (the in-repo default when present);
297
299
  // • anything else ⇒ treat it as a githolon COMMAND LINE and drive `harness-install`
298
300
  // (default `npx githolon` when the in-repo helper is absent; the test forces this lane by passing
299
301
  // e.g. `installHelper: 'node /abs/cli/dist/cli.mjs'`).
300
- final resolvedHelper =
301
- installHelper ?? '${File(entry).parent.path}/offline-install.ts';
302
+ final adjacentInstaller = '${File(entry).parent.path}/offline-install.mjs';
303
+ final resolvedHelper = installHelper ??
304
+ (File(adjacentInstaller).existsSync()
305
+ ? adjacentInstaller
306
+ : '${File(entry).parent.path}/offline-install.ts');
302
307
  final useInRepoScript =
303
- resolvedHelper.endsWith('.mjs') && File(resolvedHelper).existsSync();
308
+ (resolvedHelper.endsWith('.ts') || resolvedHelper.endsWith('.mjs')) &&
309
+ File(resolvedHelper).existsSync();
304
310
  final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
305
311
 
306
312
  // A FRESH temp dir per harness — the snapshot + any transient runner state live here and nowhere else,
@@ -308,13 +314,18 @@ class NomosTestHarness {
308
314
  final workdir = await Directory.systemTemp.createTemp('nomos-harness-');
309
315
  // Total-ordered teardown even on a mid-setup failure: build the transport lazily and clean up on throw.
310
316
  NodeRunnerTransport? transport;
311
- final phase = (String m) { if (onDiagnostic != null) onDiagnostic('[nomos-test-harness] $m'); };
317
+ final phase = (String m) {
318
+ if (onDiagnostic != null) onDiagnostic('[nomos-test-harness] $m');
319
+ };
312
320
  try {
313
321
  final snapshot = '${workdir.path}/snapshot.bin';
314
322
  final info = await _phased(
315
- phase: '1/4: offline law install (${deployJsonPaths.length} package(s))',
323
+ phase:
324
+ '1/4: offline law install (${deployJsonPaths.length} package(s))',
316
325
  workspace: workspace,
317
- awaiting: useInRepoScript ? 'offline-install.ts' : 'githolon harness-install',
326
+ awaiting: useInRepoScript
327
+ ? 'offline-install.ts'
328
+ : 'githolon harness-install',
318
329
  timeout: phaseTimeout ?? _kInstallDeadline,
319
330
  onDiagnostic: onDiagnostic,
320
331
  likelyCauses: const [
@@ -344,6 +355,11 @@ class NomosTestHarness {
344
355
  environment: {
345
356
  'NOMOS_OFFLINE': '1',
346
357
  'NOMOS_OFFLINE_RESTORE': snapshot,
358
+ 'NOMOS_OFFLINE_WASM': 'holon-testmint.wasm',
359
+ if (info['devicePublic'] != null)
360
+ 'NOMOS_TEST_K_ROOT_PUBLIC': info['devicePublic'].toString(),
361
+ if (info['deviceSecret'] != null)
362
+ 'NOMOS_TEST_ROOT_SECRET': info['deviceSecret'].toString(),
347
363
  if (runtimeDir != null) 'NOMOS_OFFLINE_RUNTIME': runtimeDir,
348
364
  },
349
365
  onDiagnostic: onDiagnostic,
@@ -355,7 +371,7 @@ class NomosTestHarness {
355
371
  timeout: phaseTimeout ?? readyTimeout,
356
372
  onDiagnostic: onDiagnostic,
357
373
  likelyCauses: const [
358
- 'node is not on PATH, or ~/.holon/runtime is cold (holon.wasm + packages.json)',
374
+ 'node is not on PATH, or ~/.holon/runtime is cold (holon-testmint.wasm + packages.json)',
359
375
  'the runner process failed to boot the engine — run with onDiagnostic to see its stderr',
360
376
  ],
361
377
  run: () => plane.ready);
@@ -370,7 +386,8 @@ class NomosTestHarness {
370
386
  'run with onDiagnostic to see where the runner blocked',
371
387
  ],
372
388
  run: () => plane.connect(
373
- cloud: 'https://offline.invalid', // ignored: the offline runner serves everything locally
389
+ cloud:
390
+ 'https://offline.invalid', // ignored: the offline runner serves everything locally
374
391
  workspace: workspace,
375
392
  clientId: clientId,
376
393
  allowUnborn: true,
@@ -379,7 +396,8 @@ class NomosTestHarness {
379
396
  // with the SAME key, so offline `.births()` offers Just Work.
380
397
  authorSecret: info['deviceSecret']?.toString(),
381
398
  ));
382
- phase('ready — session "$workspace" connected (law ${(info['lawHashes'] as List?)?.isNotEmpty == true ? (info['lawHashes'] as List).first.toString().substring(0, 12) : "installed"})');
399
+ phase(
400
+ 'ready — session "$workspace" connected (law ${(info['lawHashes'] as List?)?.isNotEmpty == true ? (info['lawHashes'] as List).first.toString().substring(0, 12) : "installed"})');
383
401
  // THE DECOY SESSION (multi-workspace by default in tests): sole-session inference is how
384
402
  // single-workspace assumptions ship — with one session connected, an op that forgot its session
385
403
  // silently works in tests and only refuses in production when the app connects its second
@@ -403,13 +421,15 @@ class NomosTestHarness {
403
421
  restoreFrom: base64Encode(await File(snapshot).readAsBytes()),
404
422
  ));
405
423
  }
406
- final harness = NomosTestHarness._(
407
- plane.session(workspace), workspace, transport, workdir, onDiagnostic, [workspace],
424
+ final harness = NomosTestHarness._(plane.session(workspace), workspace,
425
+ transport, workdir, onDiagnostic, [workspace],
408
426
  devicePublics: {
409
- if (info['devicePublic'] != null) workspace: info['devicePublic'].toString(),
427
+ if (info['devicePublic'] != null)
428
+ workspace: info['devicePublic'].toString(),
410
429
  },
411
430
  deviceSecrets: {
412
- if (info['deviceSecret'] != null) workspace: info['deviceSecret'].toString(),
431
+ if (info['deviceSecret'] != null)
432
+ workspace: info['deviceSecret'].toString(),
413
433
  },
414
434
  frameworkHash: info['frameworkHash']?.toString(),
415
435
  lawHashes: {
@@ -417,7 +437,9 @@ class NomosTestHarness {
417
437
  .map((e) => e.toString())
418
438
  .toList()
419
439
  },
420
- snapshots: {workspace: base64Encode(await File(snapshot).readAsBytes())},
440
+ snapshots: {
441
+ workspace: base64Encode(await File(snapshot).readAsBytes())
442
+ },
421
443
  installLane: _InstallLane(
422
444
  node: node,
423
445
  useInRepoScript: useInRepoScript,
@@ -485,10 +507,14 @@ class NomosTestHarness {
485
507
  }
486
508
  final entry = nodeEntry ??
487
509
  await _resolveEntry(node, resolveFrom ?? Directory.current.path);
488
- final resolvedHelper =
489
- installHelper ?? '${File(entry).parent.path}/offline-install.ts';
510
+ final adjacentInstaller = '${File(entry).parent.path}/offline-install.mjs';
511
+ final resolvedHelper = installHelper ??
512
+ (File(adjacentInstaller).existsSync()
513
+ ? adjacentInstaller
514
+ : '${File(entry).parent.path}/offline-install.ts');
490
515
  final useInRepoScript =
491
- resolvedHelper.endsWith('.mjs') && File(resolvedHelper).existsSync();
516
+ (resolvedHelper.endsWith('.ts') || resolvedHelper.endsWith('.mjs')) &&
517
+ File(resolvedHelper).existsSync();
492
518
  final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
493
519
 
494
520
  final workdir = await Directory.systemTemp.createTemp('nomos-cluster-');
@@ -499,6 +525,8 @@ class NomosTestHarness {
499
525
  final lawHashesByWs = <String, List<String>>{};
500
526
  final installDevicePublics = <String, String>{};
501
527
  final installDeviceSecrets = <String, String>{};
528
+ String? planeTestRootPublic;
529
+ String? planeTestRootSecret;
502
530
  String? frameworkHash;
503
531
  var _i = 0;
504
532
  for (final e in workspaces.entries) {
@@ -506,7 +534,9 @@ class NomosTestHarness {
506
534
  final info = await _phased(
507
535
  phase: '1: offline law install ${++_i}/${workspaces.length}',
508
536
  workspace: e.key,
509
- awaiting: useInRepoScript ? 'offline-install.ts' : 'githolon harness-install',
537
+ awaiting: useInRepoScript
538
+ ? 'offline-install.ts'
539
+ : 'githolon harness-install',
510
540
  timeout: phaseTimeout ?? _kInstallDeadline,
511
541
  onDiagnostic: onDiagnostic,
512
542
  likelyCauses: const [
@@ -530,12 +560,18 @@ class NomosTestHarness {
530
560
  frameworkHash ??= info['frameworkHash']?.toString();
531
561
  // THE HARNESS DEVICE (era-0): the install lane mints one device key per workspace — sessions sign
532
562
  // with it and the client completes the self-serve birth ceremony (enrol + cert) with the SAME key.
533
- if (info['devicePublic'] != null) installDevicePublics[e.key] = info['devicePublic'].toString();
534
- if (info['deviceSecret'] != null) installDeviceSecrets[e.key] = info['deviceSecret'].toString();
563
+ if (info['devicePublic'] != null)
564
+ installDevicePublics[e.key] = info['devicePublic'].toString();
565
+ if (info['deviceSecret'] != null)
566
+ installDeviceSecrets[e.key] = info['deviceSecret'].toString();
567
+ planeTestRootPublic ??= info['devicePublic']?.toString();
568
+ planeTestRootSecret ??= info['deviceSecret']?.toString();
535
569
  }
536
570
 
537
571
  // 2. ONE runner / ONE bridge for the whole plane — no env restore default; each connect brings its own.
538
- final phase = (String m) { if (onDiagnostic != null) onDiagnostic('[nomos-test-harness] $m'); };
572
+ final phase = (String m) {
573
+ if (onDiagnostic != null) onDiagnostic('[nomos-test-harness] $m');
574
+ };
539
575
  transport = await _phased(
540
576
  phase: '2: spawn the node runner (${workspaces.length} workspace(s))',
541
577
  awaiting: 'Process.start($node)',
@@ -547,6 +583,11 @@ class NomosTestHarness {
547
583
  node: node,
548
584
  environment: {
549
585
  'NOMOS_OFFLINE': '1',
586
+ 'NOMOS_OFFLINE_WASM': 'holon-testmint.wasm',
587
+ if (planeTestRootPublic != null)
588
+ 'NOMOS_TEST_K_ROOT_PUBLIC': planeTestRootPublic,
589
+ if (planeTestRootSecret != null)
590
+ 'NOMOS_TEST_ROOT_SECRET': planeTestRootSecret,
550
591
  if (runtimeDir != null) 'NOMOS_OFFLINE_RUNTIME': runtimeDir,
551
592
  },
552
593
  onDiagnostic: onDiagnostic,
@@ -558,7 +599,7 @@ class NomosTestHarness {
558
599
  timeout: phaseTimeout ?? readyTimeout,
559
600
  onDiagnostic: onDiagnostic,
560
601
  likelyCauses: const [
561
- 'node is not on PATH, or ~/.holon/runtime is cold (holon.wasm + packages.json)',
602
+ 'node is not on PATH, or ~/.holon/runtime is cold (holon-testmint.wasm + packages.json)',
562
603
  'the runner process failed to boot the engine — run with onDiagnostic to see its stderr',
563
604
  ],
564
605
  run: () => bridge.ready);
@@ -568,7 +609,8 @@ class NomosTestHarness {
568
609
  final snapshotB64 = <String, String>{};
569
610
  var _n = 0;
570
611
  for (final e in workspaces.entries) {
571
- snapshotB64[e.key] = base64Encode(await File(snapshots[e.key]!).readAsBytes());
612
+ snapshotB64[e.key] =
613
+ base64Encode(await File(snapshots[e.key]!).readAsBytes());
572
614
  await _phased(
573
615
  phase: '3: connect session ${++_n}/${workspaces.length}',
574
616
  workspace: e.key,
@@ -633,7 +675,8 @@ class NomosTestHarness {
633
675
  ));
634
676
  }
635
677
  if (deviceKeyed.isNotEmpty) {
636
- phase('cluster: ${deviceKeyed.length} device-keyed session(s) re-opened');
678
+ phase(
679
+ 'cluster: ${deviceKeyed.length} device-keyed session(s) re-opened');
637
680
  }
638
681
 
639
682
  final names = workspaces.keys.toList();
@@ -714,7 +757,8 @@ class NomosTestHarness {
714
757
  }
715
758
  final entry = nodeEntry ??
716
759
  await _resolveEntry(node, resolveFrom ?? Directory.current.path);
717
- final bareUid = subject.startsWith('user:') ? subject.substring(5) : subject;
760
+ final bareUid =
761
+ subject.startsWith('user:') ? subject.substring(5) : subject;
718
762
  final http = HttpClient();
719
763
  Future<Map<String, Object?>> post(String path, Object? body,
720
764
  {Map<String, String> headers = const {}}) async {
@@ -723,7 +767,8 @@ class NomosTestHarness {
723
767
  req.headers.set('x-nomos-principal', bareUid);
724
768
  if (authToken != null) req.headers.set('x-nomos-auth', authToken);
725
769
  headers.forEach(req.headers.set);
726
- if (body != null) req.add(utf8.encode(body is String ? body : jsonEncode(body)));
770
+ if (body != null)
771
+ req.add(utf8.encode(body is String ? body : jsonEncode(body)));
727
772
  final resp = await req.close();
728
773
  final text = await resp.transform(utf8.decoder).join();
729
774
  final decoded = (jsonDecode(text) as Map).cast<String, Object?>();
@@ -743,6 +788,8 @@ class NomosTestHarness {
743
788
  // server's in-process operator lane). Booting a born workspace is idempotent; an unborn one
744
789
  // refuses 409, surfaced here with the fix.
745
790
  String? frameworkHash;
791
+ final frameworkPostRoutes =
792
+ <({String domain, String directive, String path})>[];
746
793
  final lawHashes = <String>[];
747
794
  await _phased(
748
795
  phase: '1/6: boot the born platform + deploy law on the test cloud',
@@ -769,9 +816,32 @@ class NomosTestHarness {
769
816
  for (final p in deployJsonPaths) {
770
817
  final body = await File(p).readAsString();
771
818
  await post('/v2/workspaces/$platformWorkspace/domains', body);
772
- final summary = ((jsonDecode(body) as Map)['summary'] as Map?);
819
+ final deploy = (jsonDecode(body) as Map);
820
+ final summary = (deploy['summary'] as Map?);
773
821
  final h = summary?['domainHash']?.toString();
774
822
  if (h != null && h.isNotEmpty) lawHashes.add(h);
823
+ final openapi = deploy['openapi'] as Map?;
824
+ final pathSets = <Map>[
825
+ (openapi?['paths'] as Map?) ?? const {},
826
+ (((openapi?['x-nomos-framework-routes'] as Map?)?['paths'])
827
+ as Map?) ??
828
+ const {},
829
+ ];
830
+ for (final paths in pathSets) {
831
+ for (final entry in paths.entries) {
832
+ final post = (entry.value as Map?)?['post'] as Map?;
833
+ final route = post?['x-nomos'] as Map?;
834
+ final domain = route?['domain']?.toString() ?? '';
835
+ final directive = route?['directiveId']?.toString() ?? '';
836
+ if (domain.isNotEmpty && directive.isNotEmpty) {
837
+ frameworkPostRoutes.add((
838
+ domain: domain,
839
+ directive: directive,
840
+ path: entry.key.toString(),
841
+ ));
842
+ }
843
+ }
844
+ }
775
845
  }
776
846
  });
777
847
  if (frameworkHash == null || frameworkHash!.isEmpty) {
@@ -782,6 +852,27 @@ class NomosTestHarness {
782
852
  throw NomosTestHarnessException(
783
853
  'cloudBound: no deploy.json carried a summary.domainHash — cannot run the birth ceremony (lawHash unknown)');
784
854
  }
855
+ final birthRoutes = frameworkPostRoutes
856
+ .where((route) => route.directive == 'birthHome')
857
+ .toSet()
858
+ .toList();
859
+ if (birthRoutes.length != 1) {
860
+ throw NomosTestHarnessException(
861
+ 'cloudBound: the deployed package must expose exactly one semantic birthHome route; found ${birthRoutes.length}');
862
+ }
863
+ final applicationHomeDomain = birthRoutes.single.domain;
864
+ final birthHomePath = birthRoutes.single.path;
865
+ final enrollmentRoutes = frameworkPostRoutes
866
+ .where((route) =>
867
+ route.directive == 'enrollFirstDevice' &&
868
+ route.domain == applicationHomeDomain)
869
+ .toSet()
870
+ .toList();
871
+ if (enrollmentRoutes.length != 1) {
872
+ throw NomosTestHarnessException(
873
+ "cloudBound: application/home domain '$applicationHomeDomain' must expose exactly one semantic enrollFirstDevice route; found ${enrollmentRoutes.length}");
874
+ }
875
+ final enrollFirstDevicePath = enrollmentRoutes.single.path;
785
876
  // 2/6 — the LOCAL engine plane, CLOUD-BOUND, spawned BEFORE the birth: the runner's local kernel is
786
877
  // what mints the device key + signs the birth cert (era-0: a birth is ATTESTED or not at all).
787
878
  transport = await _phased(
@@ -790,7 +881,16 @@ class NomosTestHarness {
790
881
  timeout: phaseTimeout ?? _kSpawnDeadline,
791
882
  onDiagnostic: onDiagnostic,
792
883
  run: () => NodeRunnerTransport.start(
793
- entry: entry, resolveFrom: resolveFrom, node: node, onDiagnostic: onDiagnostic));
884
+ entry: entry,
885
+ resolveFrom: resolveFrom,
886
+ node: node,
887
+ environment: cloud.testKRootPublic == null
888
+ ? null
889
+ : {
890
+ ...Platform.environment,
891
+ 'NOMOS_TEST_K_ROOT_PUBLIC': cloud.testKRootPublic!,
892
+ },
893
+ onDiagnostic: onDiagnostic));
794
894
  final plane = NomosBridge(transport!);
795
895
  await _phased(
796
896
  phase: '3/6: runner ready',
@@ -804,7 +904,8 @@ class NomosTestHarness {
804
904
  // on-chain AuthProvider (enrollFirstDevice — no admin in the loop). The platform is WARRANTED, so
805
905
  // plain unsigned enrolment refuses by design; this is the one first-device door.
806
906
  final device = await _phased(
807
- phase: '4/6: device ceremony (keygen → dev-IdP token → enrollFirstDevice)',
907
+ phase:
908
+ '4/6: device ceremony (keygen → dev-IdP token → enrollFirstDevice)',
808
909
  workspace: platformWorkspace,
809
910
  awaiting: 'authorKeygen + /dev/mint-token + enrollFirstDevice',
810
911
  timeout: phaseTimeout ?? _kInstallDeadline,
@@ -826,14 +927,17 @@ class NomosTestHarness {
826
927
  final publicB64 = (kp['public'] ?? '').toString();
827
928
  final secret = (kp['secret'] ?? '').toString();
828
929
  if (publicB64.isEmpty || secret.isEmpty) {
829
- throw NomosTestHarnessException('cloudBound: authorKeygen returned no device key');
930
+ throw NomosTestHarnessException(
931
+ 'cloudBound: authorKeygen returned no device key');
830
932
  }
831
933
  final keyHash = sha256.convert(base64Decode(publicB64)).toString();
832
- final minted = await post('/dev/mint-token', {'sub': bareUid, 'nonce': keyHash});
934
+ final minted = await post(
935
+ '/dev/mint-token', {'sub': bareUid, 'nonce': keyHash});
833
936
  final idpToken = (minted['token'] ?? '').toString();
834
937
  final issuer = (minted['issuer'] ?? '').toString();
835
938
  if (idpToken.isEmpty || issuer.isEmpty) {
836
- throw NomosTestHarnessException('cloudBound: /dev/mint-token returned no token — is the test cloud started with platform:/deployJsonPath: and a law composing setAuthProvider?');
939
+ throw NomosTestHarnessException(
940
+ 'cloudBound: /dev/mint-token returned no token — is the test cloud started with platform:/deployJsonPath: and a law composing setAuthProvider?');
837
941
  }
838
942
  // THE KEYLESS ENROLL-ONCE DELEGATION (links[1] of the home cert chain) — device-self-signed,
839
943
  // its authority the dev-IdP token. enrollFirstDevice SEEDS it as the user's SignerDelegation, so
@@ -851,7 +955,8 @@ class NomosTestHarness {
851
955
  'capturedClock': DateTime.now().toUtc().millisecondsSinceEpoch,
852
956
  'epoch': 0,
853
957
  }, secret);
854
- await post('/v2/workspaces/$platformWorkspace/enrollFirstDevice', {
958
+ await post(
959
+ '/v2/workspaces/$platformWorkspace$enrollFirstDevicePath', {
855
960
  'keyHash': keyHash,
856
961
  'verifiedVia': issuer,
857
962
  'validFromEpoch': 0,
@@ -860,8 +965,16 @@ class NomosTestHarness {
860
965
  'actor': subject,
861
966
  'authorSecret': secret,
862
967
  'delegationCert': jsonEncode(delegationCert),
863
- }, headers: {'x-nomos-auth': idpToken});
864
- return (publicB64: publicB64, secret: secret, keyHash: keyHash, idpToken: idpToken);
968
+ },
969
+ headers: {
970
+ 'x-nomos-auth': idpToken
971
+ });
972
+ return (
973
+ publicB64: publicB64,
974
+ secret: secret,
975
+ keyHash: keyHash,
976
+ idpToken: idpToken
977
+ );
865
978
  });
866
979
  // 5/6 — THE HOME CEREMONY, remotely: the platform's own gateway offer births the home (the ONE
867
980
  // control-plane contact; the app lane below never touches the platform again). The offer carries
@@ -871,7 +984,7 @@ class NomosTestHarness {
871
984
  await _phased(
872
985
  phase: '5/6: remote home-birth ceremony (attested)',
873
986
  workspace: platformWorkspace,
874
- awaiting: 'POST /v2/workspaces/$platformWorkspace/birthHome',
987
+ awaiting: 'POST /v2/workspaces/$platformWorkspace$birthHomePath',
875
988
  timeout: phaseTimeout ?? _kInstallDeadline,
876
989
  onDiagnostic: onDiagnostic,
877
990
  run: () async {
@@ -886,7 +999,8 @@ class NomosTestHarness {
886
999
  'parentId': platformWorkspace,
887
1000
  'epoch': 0,
888
1001
  }, device.secret);
889
- final out = await post('/v2/workspaces/$platformWorkspace/birthHome', {
1002
+ final out =
1003
+ await post('/v2/workspaces/$platformWorkspace$birthHomePath', {
890
1004
  'owner': subject,
891
1005
  'ownerKeyHash': device.keyHash,
892
1006
  'frameworkHash': frameworkHash,
@@ -895,9 +1009,15 @@ class NomosTestHarness {
895
1009
  'homeCert': jsonEncode(homeCert),
896
1010
  'actor': subject,
897
1011
  'authorSecret': device.secret,
898
- }, headers: {'x-nomos-auth': device.idpToken});
1012
+ }, headers: {
1013
+ 'x-nomos-auth': device.idpToken
1014
+ });
899
1015
  final born = (out['born'] as List?) ?? const [];
900
- final bornNames = born.map((b) => (b as Map)['workspace']?.toString()).whereType<String>().where((w) => w.isNotEmpty).toList();
1016
+ final bornNames = born
1017
+ .map((b) => (b as Map)['workspace']?.toString())
1018
+ .whereType<String>()
1019
+ .where((w) => w.isNotEmpty)
1020
+ .toList();
901
1021
  home = bornNames.isEmpty ? null : bornNames.first;
902
1022
  if (home == null) {
903
1023
  final outcomes = jsonEncode(out['birthOutcomes'] ?? const []);
@@ -927,7 +1047,8 @@ class NomosTestHarness {
927
1047
  // NO DECOY on a cloudBound harness: the app is genuinely multi-workspace (the home plus every
928
1048
  // locally-born child), so the plane is never sole-session — the decoy the offline harness
929
1049
  // needs (to break single-session inference) would just be a spurious local-only workspace here.
930
- final _ = decoySession; // (accepted for signature parity; a cloudBound plane needs no decoy)
1050
+ final _ =
1051
+ decoySession; // (accepted for signature parity; a cloudBound plane needs no decoy)
931
1052
  final harness = NomosTestHarness._(
932
1053
  plane.session(home!),
933
1054
  home!,
@@ -951,8 +1072,12 @@ class NomosTestHarness {
951
1072
  }
952
1073
  return harness;
953
1074
  } catch (e) {
954
- try { transport?.dispose(); } catch (_) {}
955
- try { await workdir.delete(recursive: true); } catch (_) {}
1075
+ try {
1076
+ transport?.dispose();
1077
+ } catch (_) {}
1078
+ try {
1079
+ await workdir.delete(recursive: true);
1080
+ } catch (_) {}
956
1081
  rethrow;
957
1082
  } finally {
958
1083
  http.close(force: true);
@@ -970,11 +1095,13 @@ class NomosTestHarness {
970
1095
  /// until every session reports neither `pushFailed` nor `stillAhead`, or [timeout] (then throws with the
971
1096
  /// per-workspace last verdicts). Returns the final sync result per workspace.
972
1097
  Future<Map<String, Map<String, Object?>>> converge(
973
- {Duration timeout = const Duration(seconds: 120), List<String>? workspaces}) async {
1098
+ {Duration timeout = const Duration(seconds: 120),
1099
+ List<String>? workspaces}) async {
974
1100
  // Every LIVE session on the plane (not the harness's construction-time list — a locally-born child
975
1101
  // acquired after construction must converge too), PARENT-FIRST: the home syncs before its children, so
976
1102
  // its birth offer-effect creates each child's remote custody BEFORE that child pushes its own writes.
977
- final live = bridge.connectedSessions.where((w) => w != _decoyWorkspace).toList();
1103
+ final live =
1104
+ bridge.connectedSessions.where((w) => w != _decoyWorkspace).toList();
978
1105
  final hw = homeWorkspace;
979
1106
  if (hw != null && live.remove(hw)) live.insert(0, hw); // the parent leads
980
1107
  final targets = workspaces ?? live;
@@ -984,9 +1111,11 @@ class NomosTestHarness {
984
1111
  while (pending.isNotEmpty) {
985
1112
  for (final ws in List.of(pending)) {
986
1113
  try {
987
- final r = (await bridge.session(ws).sync(admit: true)).cast<String, Object?>();
1114
+ final r = (await bridge.session(ws).sync(admit: true))
1115
+ .cast<String, Object?>();
988
1116
  last[ws] = r;
989
- if (r['pushFailed'] != true && r['stillAhead'] != true) pending.remove(ws);
1117
+ if (r['pushFailed'] != true && r['stillAhead'] != true)
1118
+ pending.remove(ws);
990
1119
  } catch (e) {
991
1120
  last[ws] = {'error': e.toString()};
992
1121
  }
@@ -1056,7 +1185,9 @@ class NomosTestHarness {
1056
1185
  final info = await _phased(
1057
1186
  phase: 'seed: offline law install',
1058
1187
  workspace: workspace,
1059
- awaiting: lane.useInRepoScript ? 'offline-install.ts' : 'githolon harness-install',
1188
+ awaiting: lane.useInRepoScript
1189
+ ? 'offline-install.ts'
1190
+ : 'githolon harness-install',
1060
1191
  timeout: _phaseTimeout ?? _kInstallDeadline,
1061
1192
  onDiagnostic: _onDiagnostic,
1062
1193
  run: () => _runInstall(
@@ -1119,7 +1250,8 @@ class NomosTestHarness {
1119
1250
  /// un-synced writes + the DLQ), with its head + law hashes + capture time. The bytes are the SAME
1120
1251
  /// base64 `connect(restoreFrom:)` consumes — [restoreWorkspace] round-trips it. O(1)-ish (a tree serialize).
1121
1252
  Future<WorkspaceSnapshot> snapshotWorkspace(String workspace) async {
1122
- final s = session(workspace); // throws NomosTestHarnessException if not a connected session
1253
+ final s = session(
1254
+ workspace); // throws NomosTestHarnessException if not a connected session
1123
1255
  return _phased(
1124
1256
  phase: 'snapshotWorkspace',
1125
1257
  workspace: workspace,
@@ -1147,13 +1279,15 @@ class NomosTestHarness {
1147
1279
  for (final ws in _sessionNames) {
1148
1280
  out[ws] = await snapshotWorkspace(ws);
1149
1281
  }
1150
- return PlaneSnapshot(workspaces: Map.unmodifiable(out), capturedAt: DateTime.now().toUtc());
1282
+ return PlaneSnapshot(
1283
+ workspaces: Map.unmodifiable(out), capturedAt: DateTime.now().toUtc());
1151
1284
  }
1152
1285
 
1153
1286
  /// Restore a workspace FROM a [WorkspaceSnapshot] — re-open its session on this plane from the snapshot's
1154
1287
  /// LOCAL CUSTODY (no network clone), replacing the resident holon. The workspace need not currently be a
1155
1288
  /// session (a fresh restore is allowed); its name is taken from [snapshot] unless [into] overrides it.
1156
- Future<NomosSession> restoreWorkspace(WorkspaceSnapshot snapshot, {String? into}) async {
1289
+ Future<NomosSession> restoreWorkspace(WorkspaceSnapshot snapshot,
1290
+ {String? into}) async {
1157
1291
  final ws = into ?? snapshot.workspace;
1158
1292
  await _phased(
1159
1293
  phase: 'restoreWorkspace',
@@ -1191,7 +1325,8 @@ class NomosTestHarness {
1191
1325
  if (_disposed) return;
1192
1326
  _disposed = true;
1193
1327
  final f = _onDiagnostic;
1194
- if (f != null) f('[nomos-test-harness] phase dispose (≤${(_phaseTimeout ?? _kOtherDeadline).inSeconds}s) …');
1328
+ if (f != null)
1329
+ f('[nomos-test-harness] phase dispose (≤${(_phaseTimeout ?? _kOtherDeadline).inSeconds}s) …');
1195
1330
  try {
1196
1331
  await _transport.dispose().timeout(_phaseTimeout ?? _kOtherDeadline,
1197
1332
  onTimeout: () => throw HarnessPhaseTimeout(
@@ -1200,7 +1335,8 @@ class NomosTestHarness {
1200
1335
  elapsed: _phaseTimeout ?? _kOtherDeadline,
1201
1336
  awaiting: 'NodeRunnerTransport.dispose'));
1202
1337
  } catch (e) {
1203
- _diag('[nomos-test-harness] transport dispose error (continuing to temp-dir cleanup): $e');
1338
+ _diag(
1339
+ '[nomos-test-harness] transport dispose error (continuing to temp-dir cleanup): $e');
1204
1340
  }
1205
1341
  try {
1206
1342
  if (await _workdir.exists()) await _workdir.delete(recursive: true);
@@ -1235,9 +1371,12 @@ class NomosTestHarness {
1235
1371
  // Both lanes take the SAME flags (--deploy/--workspace/--out/--runtime) and print the SAME one-line JSON
1236
1372
  // contract; only the leading command differs (node <script> vs `<githolon...> harness-install`).
1237
1373
  final common = <String>[
1238
- '--deploy', deployJsonPaths.join(','),
1239
- '--workspace', workspace,
1240
- '--out', snapshot,
1374
+ '--deploy',
1375
+ deployJsonPaths.join(','),
1376
+ '--workspace',
1377
+ workspace,
1378
+ '--out',
1379
+ snapshot,
1241
1380
  if (runtimeDir != null) ...['--runtime', runtimeDir],
1242
1381
  ];
1243
1382
  final String executable;
@@ -1247,8 +1386,11 @@ class NomosTestHarness {
1247
1386
  args = [inRepoScript, ...common];
1248
1387
  } else {
1249
1388
  // `githolonCmd` is a command line (e.g. `npx githolon` or `node /abs/cli.mjs`) — split into tokens.
1250
- final tokens =
1251
- githolonCmd.trim().split(RegExp(r'\s+')).where((t) => t.isNotEmpty).toList();
1389
+ final tokens = githolonCmd
1390
+ .trim()
1391
+ .split(RegExp(r'\s+'))
1392
+ .where((t) => t.isNotEmpty)
1393
+ .toList();
1252
1394
  if (tokens.isEmpty) {
1253
1395
  throw NomosTestHarnessException(
1254
1396
  'no install helper resolved — pass installHelper: (an offline-install.ts path, or a githolon command)');
@@ -1335,12 +1477,15 @@ class WorkspaceSnapshot {
1335
1477
  'capturedAt': capturedAt.toUtc().toIso8601String(),
1336
1478
  };
1337
1479
 
1338
- factory WorkspaceSnapshot.fromJson(Map<String, Object?> j) => WorkspaceSnapshot(
1480
+ factory WorkspaceSnapshot.fromJson(Map<String, Object?> j) =>
1481
+ WorkspaceSnapshot(
1339
1482
  workspace: j['workspace'] as String,
1340
1483
  snapshotB64: j['snapshotB64'] as String,
1341
1484
  head: (j['head'] ?? '') as String,
1342
- lawHashes: ((j['lawHashes'] as List?) ?? const []).map((e) => '$e').toList(),
1343
- capturedAt: DateTime.parse((j['capturedAt'] ?? DateTime.now().toUtc().toIso8601String()) as String),
1485
+ lawHashes:
1486
+ ((j['lawHashes'] as List?) ?? const []).map((e) => '$e').toList(),
1487
+ capturedAt: DateTime.parse((j['capturedAt'] ??
1488
+ DateTime.now().toUtc().toIso8601String()) as String),
1344
1489
  );
1345
1490
 
1346
1491
  @override
@@ -1363,8 +1508,12 @@ class PlaneSnapshot {
1363
1508
  };
1364
1509
 
1365
1510
  factory PlaneSnapshot.fromJson(Map<String, Object?> j) => PlaneSnapshot(
1366
- workspaces: ((j['workspaces'] as Map?) ?? const {}).map(
1367
- (k, v) => MapEntry('$k', WorkspaceSnapshot.fromJson((v as Map).cast<String, Object?>()))),
1368
- capturedAt: DateTime.parse((j['capturedAt'] ?? DateTime.now().toUtc().toIso8601String()) as String),
1511
+ workspaces: ((j['workspaces'] as Map?) ?? const {}).map((k, v) =>
1512
+ MapEntry(
1513
+ '$k',
1514
+ WorkspaceSnapshot.fromJson(
1515
+ (v as Map).cast<String, Object?>()))),
1516
+ capturedAt: DateTime.parse((j['capturedAt'] ??
1517
+ DateTime.now().toUtc().toIso8601String()) as String),
1369
1518
  );
1370
1519
  }