@githolon/dsl 0.107.4 → 0.108.1

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 (93) hide show
  1. package/dart/CHANGELOG.md +24 -0
  2. package/dart/lib/src/application_framework_calls.dart +299 -0
  3. package/dart/lib/src/generated_call.dart +96 -0
  4. package/dart/lib/src/nms_generated_runtime.dart +29 -0
  5. package/dart/lib/src/node_runner_transport.dart +77 -1
  6. package/dart/lib/src/result_usda.dart +487 -0
  7. package/dart/lib/src/runner/node-entry.mjs +10634 -6122
  8. package/dart/lib/src/runner/offline-install.mjs +4187 -2716
  9. package/dart/lib/src/runtime_v2_gateway.dart +54 -0
  10. package/dart/lib/src/runtime_v2_transport.dart +117 -0
  11. package/dart/lib/src/session_bound.dart +7 -2
  12. package/dart/lib/src/test_harness.dart +373 -282
  13. package/dart/lib/src/webview_bridge.dart +2146 -725
  14. package/dart/pubspec.lock +17 -1
  15. package/dart/pubspec.yaml +3 -2
  16. package/dart/test/acquire_session_credentials_test.dart +1 -1
  17. package/dart/test/acquire_session_test.dart +68 -14
  18. package/dart/test/adv_realm_interference_test.dart +6 -4
  19. package/dart/test/cluster_births_test.dart +11 -8
  20. package/dart/test/cluster_local_autosync_test.dart +1 -1
  21. package/dart/test/cluster_scale_test.dart +3 -2
  22. package/dart/test/dlq_session_surface_test.dart +52 -35
  23. package/dart/test/first_app_gauntlet_test.dart +1 -1
  24. package/dart/test/local_sync_inert_test.dart +23 -26
  25. package/dart/test/offer_authentication_proof_test.dart +193 -0
  26. package/dart/test/result_usda_test.dart +49 -0
  27. package/dart/test/session_bound_test.dart +8 -11
  28. package/dart/test/stale_bundle_test.dart +18 -3
  29. package/dart/test/test_harness_test.dart +14 -27
  30. package/dart/test/webview_bridge_test.dart +504 -83
  31. package/dart_types/CHANGELOG.md +17 -0
  32. package/dart_types/lib/nomos_kernel_protocol.dart +11 -0
  33. package/dart_types/lib/src/custody.dart +64 -19
  34. package/dart_types/lib/src/local_work_rejected.dart +30 -45
  35. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pb.dart +169 -0
  36. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pbenum.dart +11 -0
  37. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +17945 -0
  38. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +933 -0
  39. package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pb.dart +245 -0
  40. package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pbenum.dart +160 -0
  41. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +8664 -0
  42. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pbenum.dart +369 -0
  43. package/dart_types/lib/src/surfaced.dart +30 -25
  44. package/dart_types/pubspec.lock +16 -0
  45. package/dart_types/pubspec.yaml +3 -1
  46. package/dart_types/test/surfaced_test.dart +2 -1
  47. package/package.json +1 -1
  48. package/runtime/generated-application-host.js +5760 -1879
  49. package/src/authoring.ts +3 -2
  50. package/src/birth_meter_ensure.ts +2 -0
  51. package/src/births.ts +21 -0
  52. package/src/build_package.ts +30 -0
  53. package/src/business_birth.ts +41 -35
  54. package/src/codegen_call_usda.ts +1111 -0
  55. package/src/codegen_dart.ts +207 -282
  56. package/src/codegen_dart_application.ts +195 -252
  57. package/src/codegen_dart_client.ts +1978 -593
  58. package/src/codegen_dart_framework_calls.ts +291 -0
  59. package/src/codegen_dart_scenario.ts +7 -0
  60. package/src/codegen_proof.ts +3 -4
  61. package/src/codegen_provider_dart.ts +21 -7
  62. package/src/codegen_query_usda.ts +214 -0
  63. package/src/codegen_result_usda.ts +685 -0
  64. package/src/codegen_ts.ts +3945 -1111
  65. package/src/compile_package_main.ts +392 -46
  66. package/src/directive.ts +19 -4
  67. package/src/engine_entry.ts +45 -4
  68. package/src/framework/application_home.ts +4 -0
  69. package/src/framework/auth_provider_law.ts +296 -20
  70. package/src/framework/birth_certificate_law.ts +24 -0
  71. package/src/framework/home.ts +83 -40
  72. package/src/framework/owned_custody.ts +25 -12
  73. package/src/framework/owned_custody_value.ts +51 -0
  74. package/src/framework/relations.ts +47 -21
  75. package/src/framework/signing_identity.ts +100 -17
  76. package/src/framework/workspaces.ts +39 -10
  77. package/src/framework_call_definitions.ts +53 -0
  78. package/src/framework_read_definitions.ts +45 -0
  79. package/src/identity_frontier.ts +58 -0
  80. package/src/index.ts +22 -1
  81. package/src/intent.ts +216 -16
  82. package/src/law_check.ts +19 -1
  83. package/src/law_graph.ts +57 -8
  84. package/src/law_graph_usda.ts +73 -5
  85. package/src/law_value_type.ts +106 -19
  86. package/src/read.ts +47 -0
  87. package/src/read_projection_namespace.ts +7 -0
  88. package/src/shape_compat.ts +44 -42
  89. package/src/stable_ids.ts +37 -2
  90. package/src/test_call_usda.ts +83 -0
  91. package/src/wire.ts +36 -2
  92. package/src/wire_encode.ts +45 -1
  93. package/src/workspace_sharing.ts +27 -10
@@ -41,13 +41,17 @@ import 'dart:convert';
41
41
  import 'dart:io';
42
42
 
43
43
  import 'package:crypto/crypto.dart' show sha256;
44
+ import 'package:fixnum/fixnum.dart' show Int64;
45
+ import 'package:nomos_types/nomos_kernel_protocol.dart'
46
+ show BirthCertificate, BirthOutcomeStatus, SyncWorkspaceResponse;
47
+ import 'package:nomos_types/nomos_types.dart' show Sha256Digest;
44
48
  import 'package:package_config/package_config.dart' show loadPackageConfigUri;
45
49
 
46
50
  import 'dev_cloud.dart';
51
+ import 'application_framework_calls.dart';
47
52
  import 'node_runner_transport.dart';
48
53
  import 'session_bound.dart' show DomainNotInstalledForSession;
49
- import 'webview_bridge.dart'
50
- show NomosBirthCertificateSpec, NomosBridge, NomosSession;
54
+ import 'webview_bridge.dart' show NomosBridge, NomosSession;
51
55
 
52
56
  enum _KernelRuntimeArtifact {
53
57
  executable('holon.wasm'),
@@ -68,6 +72,30 @@ enum _CandidateKernelBinary {
68
72
  final String fileName;
69
73
  }
70
74
 
75
+ /// Lower the generated protobuf certificate into the framework law's USDA-authored value shape. This is
76
+ /// deliberately not a second certificate type: callers cannot construct it independently, and every field
77
+ /// is read from the compiler-generated message.
78
+ Map<String, Object?> _birthCertificateLawValue(BirthCertificate certificate) =>
79
+ <String, Object?>{
80
+ 'child': certificate.child,
81
+ 'founder': certificate.founder,
82
+ 'verified': certificate.verified,
83
+ 'parentKey': base64Encode(certificate.parentKey),
84
+ 'parentId': certificate.parentId,
85
+ 'epoch': certificate.epoch.toInt(),
86
+ 'signature': base64Encode(certificate.signature),
87
+ if (certificate.hasVerifiedVia()) 'verifiedVia': certificate.verifiedVia,
88
+ if (certificate.hasIdpToken()) 'idpToken': certificate.idpToken,
89
+ if (certificate.hasCapturedClock())
90
+ 'capturedClock': certificate.capturedClock.toInt(),
91
+ if (certificate.hasAuthProviderIssuer())
92
+ 'authProviderIssuer': certificate.authProviderIssuer,
93
+ if (certificate.hasAuthProviderJwks())
94
+ 'authProviderJwks': certificate.authProviderJwks,
95
+ if (certificate.hasAuthProviderAudience())
96
+ 'authProviderAudience': certificate.authProviderAudience,
97
+ };
98
+
71
99
  /// A source checkout is itself a coordinated release candidate: compiler, framework packages and kernels
72
100
  /// move together. Generated scenarios must not combine those source-generated laws with a recently-written
73
101
  /// but older global runtime cache. Published packages have no release/ directory and keep the normal
@@ -104,18 +132,21 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
104
132
  if (host == null) {
105
133
  throw NomosTestHarnessException('rustc did not report its host target');
106
134
  }
107
- final result = await Process.run('cargo', [
108
- 'run',
109
- '--quiet',
110
- '--target',
111
- host,
112
- '--manifest-path',
113
- '${sourceRoot.path}/tools/kernel-resource/Cargo.toml',
114
- '--',
115
- 'read',
116
- '${sourceRoot.path}/release/kernel-candidate.usda',
117
- field,
118
- ], workingDirectory: sourceRoot.path);
135
+ final result = await Process.run(
136
+ 'cargo',
137
+ [
138
+ 'run',
139
+ '--quiet',
140
+ '--target',
141
+ host,
142
+ '--manifest-path',
143
+ '${sourceRoot.path}/tools/kernel-resource/Cargo.toml',
144
+ '--',
145
+ 'read',
146
+ '${sourceRoot.path}/release/kernel-candidate.usda',
147
+ field,
148
+ ],
149
+ workingDirectory: sourceRoot.path);
119
150
  if (result.exitCode != 0) {
120
151
  throw NomosTestHarnessException(
121
152
  'could not read typed kernel candidate: ${result.stderr}',
@@ -222,36 +253,13 @@ Future<String?> _sourceCandidateRuntime(String nodeEntry) async {
222
253
  /// Framework-internal classifier for a sync result that can never become converged by retrying. Generated
223
254
  /// scenarios use it through [NomosTestHarness.converge]; it is public only inside `package:nomos_client/src`
224
255
  /// so the false-green contract has a cheap regression test without booting a cloud.
225
- String? nmsPermanentConvergenceFailure(Map<String, Object?> result) {
226
- final admission = result['admission'];
227
- final sessions = admission is Map && admission['sessions'] is List
228
- ? admission['sessions'] as List
229
- : const [];
230
- final rejected = sessions
231
- .whereType<Map>()
232
- .expand(
233
- (session) => session['rejected'] is List
234
- ? session['rejected'] as List
235
- : const [],
236
- )
237
- .toList();
238
- final deadLetters = admission is Map && admission['deadLetters'] is List
239
- ? admission['deadLetters'] as List
240
- : const [];
241
- final converged = result['converged'];
242
- final replayDeadLetters =
243
- converged is Map && converged['deadLettered'] is List
244
- ? converged['deadLettered'] as List
245
- : const [];
246
- final pushError = result['pushError'];
247
- final refused = pushError is Map && pushError['refused'] == true;
248
- if (rejected.isEmpty &&
249
- deadLetters.isEmpty &&
250
- replayDeadLetters.isEmpty &&
251
- !refused) {
256
+ String? nmsPermanentConvergenceFailure(SyncWorkspaceResponse result) {
257
+ final transportRefused = result.hasPushError() && result.pushError.refused;
258
+ if (result.refused == 0 && !transportRefused) {
252
259
  return null;
253
260
  }
254
- return jsonEncode(result);
261
+ return 'refused=${result.refused}, '
262
+ 'push=${result.hasPushError() ? result.pushError.message : "none"}';
255
263
  }
256
264
 
257
265
  /// The offline-install lane configuration a harness keeps so the CUSTODY DIAGNOSTIC helpers
@@ -311,10 +319,10 @@ class HarnessPhaseTimeout extends NomosTestHarnessException {
311
319
  required this.awaiting,
312
320
  this.likelyCauses = const [],
313
321
  }) : super(
314
- 'phase "$phase"${workspace != null ? ' (workspace "$workspace")' : ''} '
315
- 'did not complete within ${elapsed.inSeconds}s while awaiting `$awaiting`'
316
- '${likelyCauses.isEmpty ? '' : ' — likely causes: ${likelyCauses.map((c) => '\n • $c').join()}'}',
317
- );
322
+ 'phase "$phase"${workspace != null ? ' (workspace "$workspace")' : ''} '
323
+ 'did not complete within ${elapsed.inSeconds}s while awaiting `$awaiting`'
324
+ '${likelyCauses.isEmpty ? '' : ' — likely causes: ${likelyCauses.map((c) => '\n • $c').join()}'}',
325
+ );
318
326
  @override
319
327
  String toString() => 'HarnessPhaseTimeout: $message';
320
328
  }
@@ -432,16 +440,16 @@ class NomosTestHarness {
432
440
  _CloudRestartLane? cloudRestartLane,
433
441
  this.testCloud,
434
442
  this.homeWorkspace,
435
- }) : _sessionNames = List.of(
436
- sessionNames,
437
- ), // GROWABLE — restoreWorkspace may add a fresh session
438
- _devicePublics = devicePublics,
439
- _deviceSecrets = deviceSecrets,
440
- _lawHashes = Map.of(lawHashes),
441
- _snapshots = Map.of(snapshots),
442
- _installLane = installLane,
443
- _phaseTimeout = phaseTimeout,
444
- _cloudRestartLane = cloudRestartLane;
443
+ }) : _sessionNames = List.of(
444
+ sessionNames,
445
+ ), // GROWABLE — restoreWorkspace may add a fresh session
446
+ _devicePublics = devicePublics,
447
+ _deviceSecrets = deviceSecrets,
448
+ _lawHashes = Map.of(lawHashes),
449
+ _snapshots = Map.of(snapshots),
450
+ _installLane = installLane,
451
+ _phaseTimeout = phaseTimeout,
452
+ _cloudRestartLane = cloudRestartLane;
445
453
 
446
454
  // ── THE "NO SILENT HANGS" MACHINERY — every lifecycle await runs as a named, deadlined PHASE ──
447
455
 
@@ -587,6 +595,7 @@ class NomosTestHarness {
587
595
  /// [autoTearDown] — when true (default) and [registerTearDown] is given, registers [dispose] as a teardown.
588
596
  static Future<NomosTestHarness> ephemeral({
589
597
  required List<String> lawUsdzPaths,
598
+ NmsApplicationFrameworkCalls? applicationFrameworkCalls,
590
599
  int? applicationMaxChildWorkspacesPerUser,
591
600
  String? ownerSubject,
592
601
  String workspace = 'test-ws',
@@ -609,11 +618,9 @@ class NomosTestHarness {
609
618
  'no lawUsdzPaths — pass the build/<name>.law.usdz `npx githolon compile` emits',
610
619
  );
611
620
  }
612
- final entry =
613
- nodeEntry ??
621
+ final entry = nodeEntry ??
614
622
  await _resolveEntry(node, resolveFrom ?? Directory.current.path);
615
- final effectiveRuntimeDir =
616
- runtimeDir ??
623
+ final effectiveRuntimeDir = runtimeDir ??
617
624
  Platform.environment['NOMOS_CANDIDATE_RUNTIME'] ??
618
625
  Platform.environment['NOMOS_DART_RUNTIME'] ??
619
626
  Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
@@ -626,21 +633,25 @@ class NomosTestHarness {
626
633
  // (default `npx githolon` when the in-repo helper is absent; the test forces this lane by passing
627
634
  // e.g. `installHelper: 'node /abs/cli/dist/cli.mjs'`).
628
635
  final adjacentInstaller = '${File(entry).parent.path}/offline-install.mjs';
629
- final resolvedHelper =
630
- installHelper ??
636
+ final resolvedHelper = installHelper ??
631
637
  (File(adjacentInstaller).existsSync()
632
638
  ? adjacentInstaller
633
639
  : '${File(entry).parent.path}/offline-install.ts');
634
640
  final useInRepoScript =
635
641
  (resolvedHelper.endsWith('.ts') || resolvedHelper.endsWith('.mjs')) &&
636
- File(resolvedHelper).existsSync();
642
+ File(resolvedHelper).existsSync();
637
643
  final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
638
644
 
639
645
  final owner = ownerSubject?.trim();
640
- final birthsApplicationHome =
641
- owner != null &&
646
+ final birthsApplicationHome = owner != null &&
642
647
  owner.isNotEmpty &&
643
648
  workspace == _receivingGroupNameOf(owner);
649
+ if (birthsApplicationHome && applicationFrameworkCalls == null) {
650
+ throw NomosTestHarnessException(
651
+ 'a generated application scenario must pass its compiler-generated framework calls; '
652
+ 'the harness cannot construct domain payloads',
653
+ );
654
+ }
644
655
  // A receiving-group address is not an arbitrary law-install target. It is valid only when a parent
645
656
  // births it with the attestation, signer seating and warrant ordered into genesis. Generated scenarios
646
657
  // therefore install their package on a private throwaway application parent and exercise the same local
@@ -662,9 +673,8 @@ class NomosTestHarness {
662
673
  final info = await _phased(
663
674
  phase: '1/4: offline law install (${lawUsdzPaths.length} package(s))',
664
675
  workspace: installWorkspace,
665
- awaiting: useInRepoScript
666
- ? 'offline-install.ts'
667
- : 'githolon harness-install',
676
+ awaiting:
677
+ useInRepoScript ? 'offline-install.ts' : 'githolon harness-install',
668
678
  timeout: phaseTimeout ?? _kInstallDeadline,
669
679
  onDiagnostic: onDiagnostic,
670
680
  likelyCauses: const [
@@ -781,53 +791,151 @@ class NomosTestHarness {
781
791
  // scenario carries the value from the same package declaration that compiled the
782
792
  // receiving-group plan; the harness does not choose or default it. The ordinary
783
793
  // seed offer crosses the kernel author gate before the first birth is attempted.
784
- run: () => plane
785
- .session(installWorkspace)
786
- .offerOutcome(
787
- domain: '__nomos_application_home_v1',
788
- directiveId: 'seedBirthPolicy',
794
+ run: () => plane.session(installWorkspace).offerCallUsdaOutcome(
795
+ call: applicationFrameworkCalls!.seedBirthPolicy(
796
+ domain: '__nomos_application_home_v1',
797
+ workspace: installWorkspace,
798
+ maxBirthsPerPrincipal: maxChildWorkspaces,
799
+ setBy: owner,
800
+ setAt: bornAt,
801
+ ),
802
+ actor: owner,
803
+ ),
804
+ );
805
+ final keyHash = sha256.convert(base64Decode(devicePublic)).toString();
806
+ await _phased(
807
+ phase: 'application: seat the verified parent signer',
808
+ workspace: installWorkspace,
809
+ awaiting: 'law-declared enrollSigner offer',
810
+ timeout: phaseTimeout ?? _kOtherDeadline,
811
+ onDiagnostic: onDiagnostic,
812
+ run: () => plane.session(installWorkspace).offerCallUsdaOutcome(
813
+ call: applicationFrameworkCalls!.enrollSigner(
814
+ domain: '__nomos_application_home_v1',
815
+ workspace: installWorkspace,
816
+ keyHash: keyHash,
817
+ grantedBy: owner,
818
+ grantedAt: 1,
819
+ verifiedVia: 'nomos:test-mint',
820
+ ),
789
821
  actor: owner,
790
- authorSecret: deviceSecret,
791
- payload: <String, Object?>{
792
- 'maxBirthsPerPrincipal': maxChildWorkspaces,
793
- 'setBy': owner,
794
- 'setAt': bornAt,
795
- },
796
822
  ),
797
823
  );
824
+ final delegationCert =
825
+ await plane.session(installWorkspace).signBirthCert(
826
+ BirthCertificate(
827
+ child: workspace,
828
+ founder: owner,
829
+ verified: true,
830
+ parentKey: base64Decode(devicePublic),
831
+ parentId: 'root',
832
+ epoch: Int64.ZERO,
833
+ ),
834
+ deviceSecret,
835
+ );
836
+ await _phased(
837
+ phase: 'application: record the parent-signed device delegation',
838
+ workspace: installWorkspace,
839
+ awaiting: 'law-declared enrollDelegation offer',
840
+ timeout: phaseTimeout ?? _kOtherDeadline,
841
+ onDiagnostic: onDiagnostic,
842
+ run: () => plane.session(installWorkspace).offerCallUsdaOutcome(
843
+ call: applicationFrameworkCalls!.enrollDelegation(
844
+ domain: '__nomos_application_home_v1',
845
+ workspace: installWorkspace,
846
+ user: owner,
847
+ deviceKey: devicePublic,
848
+ certificate: delegationCert,
849
+ enrolledAt: '1970-01-01T00:00:00.000Z',
850
+ ),
851
+ actor: owner,
852
+ ),
853
+ );
854
+ final parentCert = await plane.session(installWorkspace).signBirthCert(
855
+ BirthCertificate(
856
+ child: installWorkspace,
857
+ founder: owner,
858
+ verified: true,
859
+ parentKey: base64Decode(devicePublic),
860
+ parentId: 'root',
861
+ epoch: Int64.ZERO,
862
+ ),
863
+ deviceSecret,
864
+ );
865
+ await _phased(
866
+ phase: 'application: attest and warrant the application parent',
867
+ workspace: installWorkspace,
868
+ awaiting: 'typed birth attestation and genesis offers',
869
+ timeout: phaseTimeout ?? _kOtherDeadline,
870
+ onDiagnostic: onDiagnostic,
871
+ run: () async {
872
+ await plane.session(installWorkspace).offerCallUsdaOutcome(
873
+ call: applicationFrameworkCalls!.seedBirthAttestation(
874
+ domain: '__nomos_application_home_v1',
875
+ workspace: installWorkspace,
876
+ child: installWorkspace,
877
+ founder: owner,
878
+ verified: 'true',
879
+ parentId: 'root',
880
+ parentKey: devicePublic,
881
+ epoch: 0,
882
+ links: <BirthCertificate>[parentCert],
883
+ ),
884
+ actor: owner,
885
+ );
886
+ await plane.session(installWorkspace).offerCallUsdaOutcome(
887
+ call: applicationFrameworkCalls.seedGenesis(
888
+ domain: '__nomos_application_home_v1',
889
+ workspace: installWorkspace,
890
+ principal: owner,
891
+ workspaceId: installWorkspace,
892
+ bornAt: '1970-01-01T00:00:00.000Z',
893
+ ),
894
+ actor: owner,
895
+ );
896
+ },
897
+ );
898
+ final homeCert = await plane.session(installWorkspace).signBirthCert(
899
+ BirthCertificate(
900
+ child: workspace,
901
+ founder: owner,
902
+ verified: true,
903
+ parentKey: base64Decode(devicePublic),
904
+ parentId: installWorkspace,
905
+ epoch: Int64.ZERO,
906
+ ),
907
+ deviceSecret,
908
+ );
798
909
  final outcome = await _phased(
799
910
  phase: 'application: birth the signed-in application',
800
911
  workspace: workspace,
801
912
  awaiting: 'attested receiving-group birth',
802
913
  timeout: phaseTimeout ?? _kOtherDeadline,
803
914
  onDiagnostic: onDiagnostic,
804
- run: () => plane
805
- .session(installWorkspace)
806
- .offerOutcome(
807
- domain: '__nomos_application_home_v1',
808
- directiveId: 'nomosInternalBirthReceivingGroup7c1f5e',
915
+ run: () => plane.session(installWorkspace).offerCallUsdaOutcome(
916
+ call: applicationFrameworkCalls!.birthReceivingGroup(
917
+ domain: '__nomos_application_home_v1',
918
+ workspace: installWorkspace,
919
+ owner: owner,
920
+ frameworkHash: Sha256Digest(framework),
921
+ lawHash: Sha256Digest(laws.last),
922
+ bornAt: bornAt,
923
+ ),
809
924
  actor: owner,
810
925
  authorSecret: deviceSecret,
811
- payload: <String, Object?>{
812
- 'owner': owner,
813
- 'frameworkHash': framework,
814
- // Ordered application deployment is reference packages first, deferred child payload last.
815
- // The parent serves the references; the receiving group is born with only its payload law.
816
- 'lawHash': laws.last,
817
- 'bornAt': bornAt,
818
- },
926
+ birthCertificate: homeCert,
819
927
  ),
820
928
  );
821
929
  String? bornStatus;
822
930
  for (final entry in outcome.birthOutcomes) {
823
- if (entry['workspace']?.toString() == workspace) {
824
- bornStatus = entry['status']?.toString();
931
+ if (entry.workspace == workspace) {
932
+ bornStatus = entry.status.name;
825
933
  break;
826
934
  }
827
935
  }
828
936
  if (outcome.born.length != 1 ||
829
937
  outcome.born.single != workspace ||
830
- bornStatus != 'born') {
938
+ bornStatus != 'BIRTH_OUTCOME_STATUS_BORN') {
831
939
  throw NomosTestHarnessException(
832
940
  'the offline application parent did not birth "$workspace" cleanly '
833
941
  '(born=${outcome.born}, birthOutcomes=${outcome.birthOutcomes})',
@@ -870,19 +978,16 @@ class NomosTestHarness {
870
978
  // harness device through the installed framework law with the same verified provenance
871
979
  // that the private test root later attests. Production clients never manufacture this
872
980
  // marker; it exists only inside the offline test-mint ceremony.
873
- run: () => plane
874
- .session(workspace)
875
- .offerOutcome(
876
- domain: '__nomos_application_home_v1',
877
- directiveId: 'enrollSigner',
981
+ run: () => plane.session(workspace).offerCallUsdaOutcome(
982
+ call: applicationFrameworkCalls!.enrollSigner(
983
+ domain: '__nomos_application_home_v1',
984
+ workspace: workspace,
985
+ keyHash: keyHash,
986
+ grantedBy: owner,
987
+ grantedAt: 1,
988
+ verifiedVia: 'nomos:test-mint',
989
+ ),
878
990
  actor: owner,
879
- authorSecret: deviceSecret,
880
- payload: <String, Object?>{
881
- 'keyHash': keyHash,
882
- 'grantedBy': owner,
883
- 'grantedAt': 1,
884
- 'verifiedVia': 'nomos:test-mint',
885
- },
886
991
  ),
887
992
  );
888
993
  } catch (error) {
@@ -890,8 +995,8 @@ class NomosTestHarness {
890
995
  // signer route. Preserve their old harness behaviour, while keeping
891
996
  // every refusal from a current package fatal.
892
997
  if (!error.toString().contains(
893
- "no active installed law declares domain '__nomos_application_home_v1'",
894
- )) {
998
+ "no active installed law declares domain '__nomos_application_home_v1'",
999
+ )) {
895
1000
  rethrow;
896
1001
  }
897
1002
  }
@@ -904,36 +1009,34 @@ class NomosTestHarness {
904
1009
  timeout: phaseTimeout ?? _kOtherDeadline,
905
1010
  onDiagnostic: onDiagnostic,
906
1011
  run: () async {
907
- await plane
908
- .session(workspace)
909
- .offerOutcome(
910
- domain: '__nomos_application_home_v1',
911
- directiveId: 'nomosInternalSeedReceivingGroupPolicy7c1f5e',
1012
+ await plane.session(workspace).offerCallUsdaOutcome(
1013
+ call: applicationFrameworkCalls!.seedReceivingGroupPolicy(
1014
+ domain: '__nomos_application_home_v1',
1015
+ workspace: workspace,
1016
+ setAt: bornAt,
1017
+ ),
912
1018
  actor: 'host-birth',
913
- payload: <String, Object?>{'setAt': bornAt},
914
1019
  );
915
- await plane
916
- .session(workspace)
917
- .offerOutcome(
918
- domain: '__nomos_application_home_v1',
919
- directiveId: 'nomosInternalSeedReceivingGroup7c1f5e',
1020
+ await plane.session(workspace).offerCallUsdaOutcome(
1021
+ call: applicationFrameworkCalls.seedReceivingGroup(
1022
+ domain: '__nomos_application_home_v1',
1023
+ workspace: workspace,
1024
+ principal: owner,
1025
+ workspaceName: _receivingGroupNameOf(owner),
1026
+ bornAt: bornAt,
1027
+ ),
920
1028
  actor: 'host-birth',
921
- payload: <String, Object?>{
922
- 'principal': owner,
923
- 'workspaceName': _receivingGroupNameOf(owner),
924
- 'bornAt': bornAt,
925
- },
926
1029
  );
927
1030
  },
928
1031
  );
929
1032
  final parentCert = await plane.signBirthCert(
930
- NomosBirthCertificateSpec(
1033
+ BirthCertificate(
931
1034
  child: workspace,
932
1035
  founder: owner,
933
1036
  verified: true,
934
- parentKey: devicePublic,
1037
+ parentKey: base64Decode(devicePublic),
935
1038
  parentId: 'root',
936
- epoch: 0,
1039
+ epoch: Int64.ZERO,
937
1040
  ),
938
1041
  deviceSecret,
939
1042
  );
@@ -944,38 +1047,36 @@ class NomosTestHarness {
944
1047
  timeout: phaseTimeout ?? _kOtherDeadline,
945
1048
  onDiagnostic: onDiagnostic,
946
1049
  run: () async {
947
- await plane
948
- .session(workspace)
949
- .offerOutcome(
950
- domain: '__nomos_application_home_v1',
951
- directiveId: 'seedBirthAttestation',
952
- actor: owner,
953
- authorSecret: deviceSecret,
954
- payload: <String, Object?>{
955
- 'child': workspace,
956
- 'founder': owner,
957
- 'verified': 'true',
958
- 'parentId': 'root',
959
- 'parentKey': devicePublic,
960
- 'epoch': 0,
961
- 'certChain': jsonEncode(<String, Object?>{
962
- 'links': <Object?>[parentCert.toWire()],
963
- }),
964
- },
965
- );
966
- await plane
967
- .session(workspace)
968
- .offerOutcome(
969
- domain: '__nomos_application_home_v1',
970
- directiveId: 'seedGenesis',
971
- actor: owner,
972
- authorSecret: deviceSecret,
973
- payload: <String, Object?>{
974
- 'principal': owner,
975
- 'workspaceId': workspace,
976
- 'bornAt': '2000-01-01T00:00:00.000Z',
977
- },
978
- );
1050
+ await plane.session(workspace).offerOutcome(
1051
+ domain: '__nomos_application_home_v1',
1052
+ directiveId: 'seedBirthAttestation',
1053
+ actor: owner,
1054
+ authorSecret: deviceSecret,
1055
+ payload: <String, Object?>{
1056
+ 'child': workspace,
1057
+ 'founder': owner,
1058
+ 'verified': 'true',
1059
+ 'parentId': 'root',
1060
+ 'parentKey': devicePublic,
1061
+ 'epoch': 0,
1062
+ 'certChain': jsonEncode(<String, Object?>{
1063
+ 'links': <Object?>[
1064
+ _birthCertificateLawValue(parentCert),
1065
+ ],
1066
+ }),
1067
+ },
1068
+ );
1069
+ await plane.session(workspace).offerOutcome(
1070
+ domain: '__nomos_application_home_v1',
1071
+ directiveId: 'seedGenesis',
1072
+ actor: owner,
1073
+ authorSecret: deviceSecret,
1074
+ payload: <String, Object?>{
1075
+ 'principal': owner,
1076
+ 'workspaceId': workspace,
1077
+ 'bornAt': '2000-01-01T00:00:00.000Z',
1078
+ },
1079
+ );
979
1080
  },
980
1081
  );
981
1082
  } on DomainNotInstalledForSession {
@@ -993,9 +1094,9 @@ class NomosTestHarness {
993
1094
  final sessionLaw = installedHashes.isEmpty
994
1095
  ? 'installed'
995
1096
  : (birthsApplicationHome
996
- ? installedHashes.last
997
- : installedHashes.first)
998
- .substring(0, 12);
1097
+ ? installedHashes.last
1098
+ : installedHashes.first)
1099
+ .substring(0, 12);
999
1100
  phase('ready — session "$workspace" connected (law $sessionLaw)');
1000
1101
  // THE DECOY SESSION (multi-workspace by default in tests): sole-session inference is how
1001
1102
  // single-workspace assumptions ship — with one session connected, an op that forgot its session
@@ -1125,24 +1226,21 @@ class NomosTestHarness {
1125
1226
  );
1126
1227
  }
1127
1228
  }
1128
- final entry =
1129
- nodeEntry ??
1229
+ final entry = nodeEntry ??
1130
1230
  await _resolveEntry(node, resolveFrom ?? Directory.current.path);
1131
- final effectiveRuntimeDir =
1132
- runtimeDir ??
1231
+ final effectiveRuntimeDir = runtimeDir ??
1133
1232
  Platform.environment['NOMOS_CANDIDATE_RUNTIME'] ??
1134
1233
  Platform.environment['NOMOS_DART_RUNTIME'] ??
1135
1234
  Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
1136
1235
  await _sourceCandidateRuntime(entry);
1137
1236
  final adjacentInstaller = '${File(entry).parent.path}/offline-install.mjs';
1138
- final resolvedHelper =
1139
- installHelper ??
1237
+ final resolvedHelper = installHelper ??
1140
1238
  (File(adjacentInstaller).existsSync()
1141
1239
  ? adjacentInstaller
1142
1240
  : '${File(entry).parent.path}/offline-install.ts');
1143
1241
  final useInRepoScript =
1144
1242
  (resolvedHelper.endsWith('.ts') || resolvedHelper.endsWith('.mjs')) &&
1145
- File(resolvedHelper).existsSync();
1243
+ File(resolvedHelper).existsSync();
1146
1244
  final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
1147
1245
 
1148
1246
  final workdir = await Directory.systemTemp.createTemp('nomos-cluster-');
@@ -1402,12 +1500,10 @@ class NomosTestHarness {
1402
1500
  'no lawUsdzPaths — pass the build/<name>.law.usdz `npx githolon compile` emits',
1403
1501
  );
1404
1502
  }
1405
- final entry =
1406
- nodeEntry ??
1503
+ final entry = nodeEntry ??
1407
1504
  await _resolveEntry(node, resolveFrom ?? Directory.current.path);
1408
- final bareUid = subject.startsWith('user:')
1409
- ? subject.substring(5)
1410
- : subject;
1505
+ final bareUid =
1506
+ subject.startsWith('user:') ? subject.substring(5) : subject;
1411
1507
  final http = HttpClient();
1412
1508
  Future<Map<String, Object?>> post(
1413
1509
  String path,
@@ -1481,8 +1577,8 @@ class NomosTestHarness {
1481
1577
  }
1482
1578
  rethrow;
1483
1579
  }
1484
- frameworkHash = ((created['controller'] as Map?)?['domainHash'])
1485
- ?.toString();
1580
+ frameworkHash =
1581
+ ((created['controller'] as Map?)?['domainHash'])?.toString();
1486
1582
  final existing = await get(
1487
1583
  '/v2/workspaces/$platformWorkspace/domains',
1488
1584
  );
@@ -1571,7 +1667,7 @@ class NomosTestHarness {
1571
1667
  );
1572
1668
  final currentLaw =
1573
1669
  (parentDomains['currentLaw'] as Map?)?.cast<String, Object?>() ??
1574
- const <String, Object?>{};
1670
+ const <String, Object?>{};
1575
1671
  final controlHomeLawHash =
1576
1672
  currentLaw[applicationHomeDomain]?.toString() ?? '';
1577
1673
  if (controlHomeLawHash.isEmpty) {
@@ -1609,8 +1705,7 @@ class NomosTestHarness {
1609
1705
  // restart while offline must not fetch any member before it can replay the persisted ledger. Release CI
1610
1706
  // supplies the exact candidate bundle. An ordinary local test pins all three artifacts advertised by its
1611
1707
  // ephemeral cloud while still online; never combine a candidate kernel with stale framework law.
1612
- final candidateRuntimeDir =
1613
- Platform.environment['NOMOS_DART_RUNTIME'] ??
1708
+ final candidateRuntimeDir = Platform.environment['NOMOS_DART_RUNTIME'] ??
1614
1709
  Platform.environment['NOMOS_OFFLINE_RUNTIME'];
1615
1710
  final localRuntime = await Directory(
1616
1711
  '${workdir.path}/client-runtime',
@@ -1630,8 +1725,8 @@ class NomosTestHarness {
1630
1725
  _KernelRuntimeArtifact.controllerLaw:
1631
1726
  '$candidateRuntimeDir/${_KernelRuntimeArtifact.controllerLaw.fileName}',
1632
1727
  };
1633
- final hasCandidateBundle =
1634
- candidateArtifacts.length == _KernelRuntimeArtifact.values.length &&
1728
+ final hasCandidateBundle = candidateArtifacts.length ==
1729
+ _KernelRuntimeArtifact.values.length &&
1635
1730
  candidateArtifacts.values.every((path) => File(path).existsSync());
1636
1731
  if (hasCandidateBundle) {
1637
1732
  for (final artifact in candidateArtifacts.entries) {
@@ -1785,17 +1880,17 @@ class NomosTestHarness {
1785
1880
  'validFromEpoch': 0,
1786
1881
  'grantedAt': DateTime.now().toUtc().toIso8601String(),
1787
1882
  'idpToken': idpToken,
1788
- 'delegationCert': jsonEncode(delegationCert),
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),
1789
1889
  },
1790
1890
  );
1791
- final enrollmentSync = await plane.sync(
1891
+ await plane.sync(
1792
1892
  session: platformAuthorSession,
1793
1893
  );
1794
- if (enrollmentSync['ok'] == false) {
1795
- throw NomosTestHarnessException(
1796
- 'cloudBound: sealed enrollFirstDevice offer did not converge: $enrollmentSync',
1797
- );
1798
- }
1799
1894
  return (
1800
1895
  publicB64: publicB64,
1801
1896
  secret: secret,
@@ -1821,36 +1916,34 @@ class NomosTestHarness {
1821
1916
  // authenticated platform holon mounted. Bind this parent certificate operation to the
1822
1917
  // platform session explicitly; relying on the single-session convenience API becomes
1823
1918
  // ambiguous as soon as the ceremony correctly opens the target workspace in-kernel.
1824
- final homeCert = await plane
1825
- .session('$clientId-platform-author')
1826
- .signBirthCert(
1827
- NomosBirthCertificateSpec(
1828
- child: homeName,
1829
- founder: subject,
1830
- // The cert's `verified` must MATCH the gate verdict (CAS-as-law): the offer carries
1831
- // the dev-IdP token, so the gate resolves a VERIFIED founder.
1832
- verified: true,
1833
- parentKey: device.publicB64,
1834
- parentId: platformWorkspace,
1835
- epoch: 0,
1836
- ),
1837
- device.secret,
1838
- );
1839
- final localBirth = await plane
1840
- .session('$clientId-platform-author')
1841
- .offerOutcome(
1842
- domain: applicationHomeDomain,
1843
- directiveId: 'nomosInternalBirthReceivingGroup7c1f5e',
1844
- actor: subject,
1845
- authorSecret: device.secret,
1846
- payload: <String, Object?>{
1847
- 'owner': subject,
1848
- 'frameworkHash': frameworkHash,
1849
- 'lawHash': applicationHomeLawHash,
1850
- 'bornAt': DateTime.now().toUtc().toIso8601String(),
1851
- },
1852
- birthCertificate: homeCert,
1853
- );
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
+ );
1854
1947
  if (!localBirth.born.contains(homeName)) {
1855
1948
  throw NomosTestHarnessException(
1856
1949
  'cloudBound: the local parent kernel admitted the receiving-group offer but did not birth '
@@ -1860,27 +1953,24 @@ class NomosTestHarness {
1860
1953
  final birthSync = await plane.sync(
1861
1954
  session: '$clientId-platform-author',
1862
1955
  );
1863
- if (birthSync['pushFailed'] == true ||
1864
- birthSync['diverged'] == true ||
1865
- birthSync['stillAhead'] == true) {
1956
+ if (birthSync.pushFailed ||
1957
+ birthSync.diverged ||
1958
+ birthSync.stillAhead) {
1866
1959
  throw NomosTestHarnessException(
1867
1960
  'cloudBound: the sealed receiving-group offer did not converge: $birthSync',
1868
1961
  );
1869
1962
  }
1870
- final remoteOutcomes =
1871
- ((birthSync['birthOutcomes'] as List?) ?? const [])
1872
- .whereType<Map>()
1873
- .map((entry) => entry.cast<String, Object?>())
1874
- .toList();
1963
+ final remoteOutcomes = birthSync.birthOutcomes;
1875
1964
  final failedRemoteBirth = remoteOutcomes.where(
1876
1965
  (entry) =>
1877
- entry['workspace']?.toString() == homeName &&
1878
- entry['status']?.toString() != 'born',
1966
+ entry.hasWorkspace() &&
1967
+ entry.workspace == homeName &&
1968
+ entry.status != BirthOutcomeStatus.BIRTH_OUTCOME_STATUS_BORN,
1879
1969
  );
1880
1970
  if (failedRemoteBirth.isNotEmpty) {
1881
1971
  throw NomosTestHarnessException(
1882
1972
  'cloudBound: the remote parent replay refused the receiving-group birth: '
1883
- '${jsonEncode(failedRemoteBirth.toList())}',
1973
+ '${failedRemoteBirth.toList()}',
1884
1974
  );
1885
1975
  }
1886
1976
  home = homeName;
@@ -1977,7 +2067,7 @@ class NomosTestHarness {
1977
2067
  /// each locally-born child's remote custody BEFORE that child's own sync converges against it. Repeats
1978
2068
  /// until every session reports neither `pushFailed` nor `stillAhead`, or [timeout] (then throws with the
1979
2069
  /// per-workspace last verdicts). Returns the final sync result per workspace.
1980
- Future<Map<String, Map<String, Object?>>> converge({
2070
+ Future<Map<String, SyncWorkspaceResponse>> converge({
1981
2071
  Duration timeout = const Duration(seconds: 120),
1982
2072
  List<String>? workspaces,
1983
2073
  }) async {
@@ -1988,37 +2078,35 @@ class NomosTestHarness {
1988
2078
  final hw = homeWorkspace;
1989
2079
  if (hw != null && live.remove(hw)) live.insert(0, hw); // the parent leads
1990
2080
  final targets = workspaces ?? live;
1991
- final last = <String, Map<String, Object?>>{};
2081
+ final last = <String, SyncWorkspaceResponse>{};
2082
+ final lastErrors = <String, String>{};
1992
2083
  final pending = List.of(targets);
1993
2084
  final deadline = DateTime.now().add(timeout);
1994
2085
  while (pending.isNotEmpty) {
1995
2086
  for (final ws in List.of(pending)) {
1996
2087
  try {
1997
- final r = (await bridge.session(ws).sync(admit: true))
1998
- .cast<String, Object?>();
2088
+ final r = await bridge.session(ws).sync(admit: true);
1999
2089
  last[ws] = r;
2090
+ lastErrors.remove(ws);
2000
2091
  final permanentFailure = nmsPermanentConvergenceFailure(r);
2001
2092
  if (permanentFailure != null) {
2002
2093
  throw NomosTestHarnessException(
2003
2094
  'converge: $ws rejected or dead-lettered business work — $permanentFailure',
2004
2095
  );
2005
2096
  }
2006
- if (r['pushFailed'] != true &&
2007
- r['stillAhead'] != true &&
2008
- r['offline'] != true &&
2009
- r['deferred'] != true)
2097
+ if (!r.pushFailed && !r.stillAhead && !r.offline && r.deferred == 0)
2010
2098
  pending.remove(ws);
2011
2099
  } on NomosTestHarnessException {
2012
2100
  rethrow;
2013
2101
  } catch (e) {
2014
- last[ws] = {'error': e.toString()};
2102
+ lastErrors[ws] = e.toString();
2015
2103
  }
2016
2104
  }
2017
2105
  if (pending.isEmpty) break;
2018
2106
  if (DateTime.now().isAfter(deadline)) {
2019
2107
  throw NomosTestHarnessException(
2020
2108
  'converge: ${pending.length} workspace(s) did not converge within ${timeout.inSeconds}s — '
2021
- '${pending.map((w) => '$w: ${jsonEncode(last[w] ?? {})}').join('; ')}',
2109
+ '${pending.map((w) => '$w: ${lastErrors[w] ?? last[w]?.toString() ?? "no result"}').join('; ')}',
2022
2110
  );
2023
2111
  }
2024
2112
  await Future<void>.delayed(const Duration(milliseconds: 500));
@@ -2418,14 +2506,14 @@ class NomosTestHarness {
2418
2506
  // transport first makes its incoming stream wait on consumers that can no longer receive their
2419
2507
  // unwatch/done messages—a deterministic teardown deadlock in reactive Flutter tests.
2420
2508
  await _plane.dispose().timeout(
2421
- _phaseTimeout ?? _kOtherDeadline,
2422
- onTimeout: () => throw HarnessPhaseTimeout(
2423
- phase: 'dispose: close bridge watches',
2424
- workspace: workspace,
2425
- elapsed: _phaseTimeout ?? _kOtherDeadline,
2426
- awaiting: 'NomosBridge.dispose',
2427
- ),
2428
- );
2509
+ _phaseTimeout ?? _kOtherDeadline,
2510
+ onTimeout: () => throw HarnessPhaseTimeout(
2511
+ phase: 'dispose: close bridge watches',
2512
+ workspace: workspace,
2513
+ elapsed: _phaseTimeout ?? _kOtherDeadline,
2514
+ awaiting: 'NomosBridge.dispose',
2515
+ ),
2516
+ );
2429
2517
  } catch (e) {
2430
2518
  _diag(
2431
2519
  '[nomos-test-harness] bridge dispose error (continuing to runner cleanup): $e',
@@ -2433,14 +2521,14 @@ class NomosTestHarness {
2433
2521
  }
2434
2522
  try {
2435
2523
  await _transport.dispose().timeout(
2436
- _phaseTimeout ?? _kOtherDeadline,
2437
- onTimeout: () => throw HarnessPhaseTimeout(
2438
- phase: 'dispose: stop the node runner',
2439
- workspace: workspace,
2440
- elapsed: _phaseTimeout ?? _kOtherDeadline,
2441
- awaiting: 'NodeRunnerTransport.dispose',
2442
- ),
2443
- );
2524
+ _phaseTimeout ?? _kOtherDeadline,
2525
+ onTimeout: () => throw HarnessPhaseTimeout(
2526
+ phase: 'dispose: stop the node runner',
2527
+ workspace: workspace,
2528
+ elapsed: _phaseTimeout ?? _kOtherDeadline,
2529
+ awaiting: 'NodeRunnerTransport.dispose',
2530
+ ),
2531
+ );
2444
2532
  } catch (e) {
2445
2533
  _diag(
2446
2534
  '[nomos-test-harness] transport dispose error (continuing to temp-dir cleanup): $e',
@@ -2574,10 +2662,13 @@ class NomosTestHarness {
2574
2662
  '@githolon/client/runner/node-entry.mjs',
2575
2663
  '@githolon/client/runner/node-entry.ts',
2576
2664
  ]) {
2577
- final r = await Process.run(node, [
2578
- '-p',
2579
- "require.resolve('$entry')",
2580
- ], workingDirectory: from);
2665
+ final r = await Process.run(
2666
+ node,
2667
+ [
2668
+ '-p',
2669
+ "require.resolve('$entry')",
2670
+ ],
2671
+ workingDirectory: from);
2581
2672
  final out = (r.stdout as String).trim();
2582
2673
  if (r.exitCode == 0 && out.isNotEmpty) return out;
2583
2674
  }
@@ -2610,21 +2701,20 @@ class WorkspaceSnapshot {
2610
2701
 
2611
2702
  /// JSON round-trip so a staged snapshot persists to disk verbatim (the bytes + provenance).
2612
2703
  Map<String, Object?> toJson() => {
2613
- 'workspace': workspace,
2614
- 'snapshotB64': snapshotB64,
2615
- 'head': head,
2616
- 'lawHashes': lawHashes,
2617
- 'capturedAt': capturedAt.toUtc().toIso8601String(),
2618
- };
2704
+ 'workspace': workspace,
2705
+ 'snapshotB64': snapshotB64,
2706
+ 'head': head,
2707
+ 'lawHashes': lawHashes,
2708
+ 'capturedAt': capturedAt.toUtc().toIso8601String(),
2709
+ };
2619
2710
 
2620
2711
  factory WorkspaceSnapshot.fromJson(Map<String, Object?> j) =>
2621
2712
  WorkspaceSnapshot(
2622
2713
  workspace: j['workspace'] as String,
2623
2714
  snapshotB64: j['snapshotB64'] as String,
2624
2715
  head: (j['head'] ?? '') as String,
2625
- lawHashes: ((j['lawHashes'] as List?) ?? const [])
2626
- .map((e) => '$e')
2627
- .toList(),
2716
+ lawHashes:
2717
+ ((j['lawHashes'] as List?) ?? const []).map((e) => '$e').toList(),
2628
2718
  capturedAt: DateTime.parse(
2629
2719
  (j['capturedAt'] ?? DateTime.now().toUtc().toIso8601String())
2630
2720
  as String,
@@ -2646,19 +2736,20 @@ class PlaneSnapshot {
2646
2736
  const PlaneSnapshot({required this.workspaces, required this.capturedAt});
2647
2737
 
2648
2738
  Map<String, Object?> toJson() => {
2649
- 'workspaces': workspaces.map((k, v) => MapEntry(k, v.toJson())),
2650
- 'capturedAt': capturedAt.toUtc().toIso8601String(),
2651
- };
2739
+ 'workspaces': workspaces.map((k, v) => MapEntry(k, v.toJson())),
2740
+ 'capturedAt': capturedAt.toUtc().toIso8601String(),
2741
+ };
2652
2742
 
2653
2743
  factory PlaneSnapshot.fromJson(Map<String, Object?> j) => PlaneSnapshot(
2654
- workspaces: ((j['workspaces'] as Map?) ?? const {}).map(
2655
- (k, v) => MapEntry(
2656
- '$k',
2657
- WorkspaceSnapshot.fromJson((v as Map).cast<String, Object?>()),
2658
- ),
2659
- ),
2660
- capturedAt: DateTime.parse(
2661
- (j['capturedAt'] ?? DateTime.now().toUtc().toIso8601String()) as String,
2662
- ),
2663
- );
2744
+ workspaces: ((j['workspaces'] as Map?) ?? const {}).map(
2745
+ (k, v) => MapEntry(
2746
+ '$k',
2747
+ WorkspaceSnapshot.fromJson((v as Map).cast<String, Object?>()),
2748
+ ),
2749
+ ),
2750
+ capturedAt: DateTime.parse(
2751
+ (j['capturedAt'] ?? DateTime.now().toUtc().toIso8601String())
2752
+ as String,
2753
+ ),
2754
+ );
2664
2755
  }