@githolon/dsl 0.100.6 → 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 (82) hide show
  1. package/dart/CHANGELOG.md +6 -0
  2. package/dart/lib/src/dev_cloud.dart +46 -15
  3. package/dart/lib/src/test_harness.dart +78 -16
  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/bundle_version_pairing_test.dart +11 -22
  8. package/dart/test/co2_product_gauntlet_test.dart +228 -0
  9. package/dart/test/errors_test.dart +63 -12
  10. package/dart/test/first_app_gauntlet_test.dart +29 -3
  11. package/dart/test/webview_bridge_test.dart +274 -0
  12. package/dart_types/CHANGELOG.md +6 -0
  13. package/dart_types/lib/src/errors.dart +136 -30
  14. package/dart_types/lib/src/surfaced.dart +48 -2
  15. package/dart_types/lib/src/wire.dart +3 -1
  16. package/dart_types/pubspec.yaml +1 -1
  17. package/dart_types/test/surfaced_test.dart +19 -0
  18. package/package.json +1 -1
  19. package/src/activity.ts +43 -4
  20. package/src/aggregate.ts +132 -22
  21. package/src/attested_core.ts +1 -1
  22. package/src/authoring.ts +119 -6
  23. package/src/birth_meter.ts +4 -4
  24. package/src/birth_meter_ensure.ts +9 -1
  25. package/src/births.ts +53 -22
  26. package/src/build_package.ts +55 -14
  27. package/src/business_birth.ts +48 -7
  28. package/src/codegen_dart.ts +267 -37
  29. package/src/codegen_dart_client.ts +921 -82
  30. package/src/codegen_openapi.ts +28 -5
  31. package/src/codegen_proof.ts +107 -11
  32. package/src/codegen_ts.ts +961 -135
  33. package/src/compile_package_main.ts +155 -45
  34. package/src/conflicts.ts +252 -0
  35. package/src/describe_model.ts +448 -0
  36. package/src/directive.ts +5 -38
  37. package/src/drivers.ts +12 -4
  38. package/src/engine_entry.ts +4 -0
  39. package/src/evolve_preview.ts +72 -11
  40. package/src/fields.ts +201 -55
  41. package/src/framework/application_home.ts +103 -0
  42. package/src/framework/application_home_domain.ts +2 -0
  43. package/src/framework/attested_receipts.ts +2 -2
  44. package/src/framework/auth_provider_law.ts +51 -24
  45. package/src/framework/business_corrections.ts +170 -0
  46. package/src/framework/disclosure.ts +1 -1
  47. package/src/framework/domain_lifecycle.ts +250 -16
  48. package/src/framework/encryption_identity.ts +1 -1
  49. package/src/framework/encryption_scope.ts +21 -13
  50. package/src/framework/era_certification.ts +1 -1
  51. package/src/framework/framework_aggregates.ts +2 -0
  52. package/src/framework/genesis.ts +1 -1
  53. package/src/framework/home.ts +22 -9
  54. package/src/framework/identity.ts +5 -5
  55. package/src/framework/impure_capability.ts +2 -2
  56. package/src/framework/owned_custody.ts +561 -0
  57. package/src/framework/platform_admin.ts +1 -1
  58. package/src/framework/relations.ts +9 -1
  59. package/src/framework/repair.ts +1 -1
  60. package/src/framework/signing_identity.ts +2 -2
  61. package/src/framework/source_roster.ts +1 -1
  62. package/src/framework/workspaces.ts +17 -10
  63. package/src/generated_client_names.ts +39 -0
  64. package/src/index.ts +20 -15
  65. package/src/kernel_capabilities.ts +25 -1
  66. package/src/law_check.ts +4 -1
  67. package/src/manifest.ts +356 -63
  68. package/src/read_projection_manifest.ts +8 -0
  69. package/src/read_projection_namespace.ts +4 -0
  70. package/src/relation.ts +1 -1
  71. package/src/shape_compat.ts +47 -2
  72. package/src/stable_ids.ts +38 -6
  73. package/src/stable_ids_types.ts +30 -0
  74. package/src/surfaced_lint.ts +0 -0
  75. package/src/usd.ts +62 -11
  76. package/src/usd_layers.ts +54 -3
  77. package/src/value_object.ts +23 -19
  78. package/src/wire.ts +1 -0
  79. package/src/wire_encode.ts +48 -4
  80. package/src/workspace_routing.ts +57 -0
  81. package/src/workspace_sharding.ts +9 -9
  82. package/src/certified_read.ts +0 -313
@@ -48,7 +48,7 @@ const int kWebviewBridgeVersion = 1;
48
48
  /// pre-stamp bundle, which reports nothing) is a stale baked asset and fails loudly at boot.
49
49
  /// MUST move with the paired @githolon/client release — a release-hygiene test pairs it with the
50
50
  /// runner stamp (the pubspec-pins pattern).
51
- const String kExpectedRunnerBundleVersion = '0.96.0';
51
+ const String kExpectedRunnerBundleVersion = '0.100.6';
52
52
 
53
53
  /// A STALE BAKED RUNNER BUNDLE: the app's built assets carry an older nomos-runner.js than this
54
54
  /// nomos_client release ships — Flutter's build cache kept it across a pub upgrade. Thrown from
@@ -87,16 +87,52 @@ class TransferProgress {
87
87
  final int received;
88
88
  final int? total;
89
89
  final String kind; // 'delta' (incremental) | 'full' (initial clone)
90
- const TransferProgress({required this.received, this.total, this.kind = 'delta'});
91
- double? get fraction => (total == null || total == 0) ? null : (received / total!).clamp(0.0, 1.0);
90
+ const TransferProgress(
91
+ {required this.received, this.total, this.kind = 'delta'});
92
+ double? get fraction => (total == null || total == 0)
93
+ ? null
94
+ : (received / total!).clamp(0.0, 1.0);
92
95
  bool get done => total != null && received >= total!;
93
- factory TransferProgress.fromMessage(Map<String, Object?> m) => TransferProgress(
96
+ factory TransferProgress.fromMessage(Map<String, Object?> m) =>
97
+ TransferProgress(
94
98
  received: (m['received'] as num?)?.toInt() ?? 0,
95
99
  total: (m['total'] as num?)?.toInt(),
96
100
  kind: m['kind']?.toString() ?? 'delta',
97
101
  );
98
102
  }
99
103
 
104
+ /// One opaque point in this workspace's verified history. Applications use it as the handle passed to a
105
+ /// generated client's `past.at(moment)`; [id] is framework identity, not a business field and need never be
106
+ /// parsed. The remaining fields make a timeline useful without exposing commit or event-sourcing ceremony.
107
+ class NomosMoment {
108
+ final String id;
109
+ final DateTime? occurredAt;
110
+ final String? actorSubject;
111
+ final String? domain;
112
+ final String? action;
113
+
114
+ const NomosMoment({
115
+ required this.id,
116
+ this.occurredAt,
117
+ this.actorSubject,
118
+ this.domain,
119
+ this.action,
120
+ });
121
+
122
+ factory NomosMoment.fromJson(Map<String, Object?> json) {
123
+ final at = json['at'];
124
+ return NomosMoment(
125
+ id: (json['oid'] ?? '').toString(),
126
+ occurredAt: at is num
127
+ ? DateTime.fromMillisecondsSinceEpoch(at.toInt(), isUtc: true)
128
+ : null,
129
+ actorSubject: json['actorSubject']?.toString(),
130
+ domain: json['domain']?.toString(),
131
+ action: json['directiveId']?.toString(),
132
+ );
133
+ }
134
+ }
135
+
100
136
  enum SyncPhase { idle, syncing, transferring, behind, offline, error }
101
137
 
102
138
  /// The app-ready sync state — a truthful aggregation of the low-level signals (sync rounds, the doorbell,
@@ -123,9 +159,17 @@ class SyncStatus {
123
159
  this.lastError,
124
160
  });
125
161
  SyncStatus copyWith({
126
- SyncPhase? phase, int? ahead, int? behind, int? saved, int? onMain, int? deadLetters,
127
- TransferProgress? transfer, bool clearTransfer = false, DateTime? lastSyncedAt,
128
- String? lastError, bool clearError = false,
162
+ SyncPhase? phase,
163
+ int? ahead,
164
+ int? behind,
165
+ int? saved,
166
+ int? onMain,
167
+ int? deadLetters,
168
+ TransferProgress? transfer,
169
+ bool clearTransfer = false,
170
+ DateTime? lastSyncedAt,
171
+ String? lastError,
172
+ bool clearError = false,
129
173
  }) =>
130
174
  SyncStatus(
131
175
  phase: phase ?? this.phase,
@@ -149,7 +193,8 @@ class NomosCreated {
149
193
  /// The workspace the offer ran on (NomosRef). Additive — defaults to '' on older runners that
150
194
  /// don't stamp it; then [ref] is workspace-blank (still a valid locator on the local plane).
151
195
  final String workspace;
152
- const NomosCreated({required this.type, required this.id, this.workspace = ''});
196
+ const NomosCreated(
197
+ {required this.type, required this.id, this.workspace = ''});
153
198
 
154
199
  /// The canonical [NomosRef] view — the `{workspace,type,id}` triple, followable/serializable
155
200
  /// (`nomos://<workspace>/<type>/<id>`). One reference, everywhere.
@@ -186,13 +231,17 @@ class NomosBbox {
186
231
  maxY = (maxY ?? maxLat)!;
187
232
 
188
233
  /// Deprecated geographic accessors — spatial axes are generic. Prefer [minX]/[minY]/[maxX]/[maxY].
189
- @Deprecated('Spatial axes are generic; use minX/minY. Kept for source compat.')
234
+ @Deprecated(
235
+ 'Spatial axes are generic; use minX/minY. Kept for source compat.')
190
236
  double get minLng => minX;
191
- @Deprecated('Spatial axes are generic; use minX/minY. Kept for source compat.')
237
+ @Deprecated(
238
+ 'Spatial axes are generic; use minX/minY. Kept for source compat.')
192
239
  double get minLat => minY;
193
- @Deprecated('Spatial axes are generic; use maxX/maxY. Kept for source compat.')
240
+ @Deprecated(
241
+ 'Spatial axes are generic; use maxX/maxY. Kept for source compat.')
194
242
  double get maxLng => maxX;
195
- @Deprecated('Spatial axes are generic; use maxX/maxY. Kept for source compat.')
243
+ @Deprecated(
244
+ 'Spatial axes are generic; use maxX/maxY. Kept for source compat.')
196
245
  double get maxLat => maxY;
197
246
 
198
247
  Map<String, Object?> toJson() => {
@@ -256,15 +305,18 @@ class NomosCompat {
256
305
 
257
306
  static NomosCompat? fromJson(Map<String, Object?>? m) {
258
307
  if (m == null) return null;
259
- final rawMembers = (m['members'] as Map?)?.cast<String, Object?>() ?? const {};
308
+ final rawMembers =
309
+ (m['members'] as Map?)?.cast<String, Object?>() ?? const {};
260
310
  final members = <String, NomosCompatMember>{};
261
311
  rawMembers.forEach((k, v) {
262
- if (v is Map) members[k] = NomosCompatMember.fromJson(v.cast<String, Object?>());
312
+ if (v is Map)
313
+ members[k] = NomosCompatMember.fromJson(v.cast<String, Object?>());
263
314
  });
264
315
  final install = (m['install'] as Map?)?.cast<String, Object?>() ?? const {};
265
316
  return NomosCompat(
266
317
  generatedFor: (m['generatedFor'] as String?) ?? '',
267
- workspaceRuns: ((m['workspaceRuns'] as List?) ?? const []).map((e) => '$e').toList(),
318
+ workspaceRuns:
319
+ ((m['workspaceRuns'] as List?) ?? const []).map((e) => '$e').toList(),
268
320
  matches: m['matches'] == true,
269
321
  offerKnown: m['offerKnown'] == true,
270
322
  structurallyCompatible: m['structurallyCompatible'] as bool?,
@@ -285,9 +337,96 @@ class NomosScopeKey {
285
337
  final String scope;
286
338
  final int epoch;
287
339
  final String key;
340
+
341
+ /// Framework-private signing-key subject used to pin `.owned()` custody to one approved device.
342
+ final String? ownedKeySubject;
343
+
344
+ /// Framework-private encrypted handoff, present only while seating the first device.
345
+ final String? ownedCustody;
346
+
347
+ /// Framework-private deterministic child address, present only for an unborn owned business aggregate.
348
+ final String? workspace;
288
349
  const NomosScopeKey(
289
- {required this.scope, required this.epoch, required this.key});
290
- Map<String, Object?> toJson() => {'scope': scope, 'epoch': epoch, 'key': key};
350
+ {required this.scope,
351
+ required this.epoch,
352
+ required this.key,
353
+ this.ownedKeySubject,
354
+ this.ownedCustody,
355
+ this.workspace});
356
+ Map<String, Object?> toJson() => {
357
+ 'scope': scope,
358
+ 'epoch': epoch,
359
+ 'key': key,
360
+ if (ownedKeySubject != null) 'ownedKeySubject': ownedKeySubject,
361
+ if (ownedCustody != null) 'ownedCustody': ownedCustody,
362
+ if (workspace != null) 'workspace': workspace,
363
+ };
364
+ }
365
+
366
+ /// The entire app-facing multi-device surface for owned business state. The request is deliberately an
367
+ /// opaque string: apps may copy, share or QR it, but never parse certificates, key material or signer law.
368
+ class NomosDevices {
369
+ final NomosBridge bridge;
370
+ final String domain;
371
+ final String domainHash;
372
+ final String? actor;
373
+ final String scope;
374
+
375
+ const NomosDevices({
376
+ required this.bridge,
377
+ required this.domain,
378
+ required this.domainHash,
379
+ required this.actor,
380
+ required this.scope,
381
+ });
382
+
383
+ Future<String> request() => bridge.nomosOwnedDeviceRequest(scope);
384
+
385
+ Future<NomosOfferOutcome> approve(String request) async {
386
+ final principal = actor ?? bridge.principal;
387
+ if (principal == null || principal.trim().isEmpty) {
388
+ throw StateError('devices.approve() needs a signed-in Nomos session');
389
+ }
390
+ final payload =
391
+ await bridge.nomosPrepareOwnedDeviceApproval(request, scope);
392
+ return bridge.offerOutcome(
393
+ domain: domain,
394
+ directiveId: 'nomos_internal_approve_owned_device_7c1f5e',
395
+ payload: payload,
396
+ actor: principal,
397
+ domainHash: domainHash,
398
+ );
399
+ }
400
+ }
401
+
402
+ /// The one business-level recovery policy surface. Escrow wraps, Orders, Receipts and retries stay inside Nomos.
403
+ class NomosRecovery {
404
+ final NomosBridge bridge;
405
+ final String domain;
406
+ final String domainHash;
407
+ final String scope;
408
+
409
+ const NomosRecovery({
410
+ required this.bridge,
411
+ required this.domain,
412
+ required this.domainHash,
413
+ required this.scope,
414
+ });
415
+
416
+ /// Opt into all-device-loss recovery with [person]. This is intentionally one-way for a workspace:
417
+ /// a principal who has legitimately recovered a key cannot be made to forget it later.
418
+ Future<Map<String, Object?>> protectWith(String person) {
419
+ if (person.trim().isEmpty) {
420
+ throw ArgumentError(
421
+ 'recovery.protectWith() needs the person who will recover you');
422
+ }
423
+ return bridge.nomosProtectOwnedIdentity(
424
+ scope: scope,
425
+ domain: domain,
426
+ domainHash: domainHash,
427
+ recoveryPrincipal: person,
428
+ );
429
+ }
291
430
  }
292
431
 
293
432
  /// The app-supplied KEY CUSTODY surface (E2E sealing). Supply one to a generated `<Domain>Client` and its
@@ -341,9 +480,7 @@ class WrappedKeyStore implements NomosKeyStore {
341
480
  hpkeEpk: d['hpkeEpk'].toString(),
342
481
  ct: d['ct'].toString());
343
482
  held.add(NomosScopeKey(
344
- scope: scope,
345
- epoch: (d['epoch'] as num?)?.toInt() ?? 0,
346
- key: key));
483
+ scope: scope, epoch: (d['epoch'] as num?)?.toInt() ?? 0, key: key));
347
484
  }
348
485
  held.sort((a, b) => b.epoch - a.epoch);
349
486
  return held;
@@ -416,7 +553,10 @@ class BirthIncomplete implements Exception {
416
553
 
417
554
  /// The first failed child's kernel code (e.g. `BirthLawPackageUnavailable`), or `BirthFailed`.
418
555
  String get code =>
419
- (failedBirths.isNotEmpty ? failedBirths.first['code']?.toString() : null) ?? 'BirthFailed';
556
+ (failedBirths.isNotEmpty
557
+ ? failedBirths.first['code']?.toString()
558
+ : null) ??
559
+ 'BirthFailed';
420
560
 
421
561
  /// The child workspace name(s) that did not come up.
422
562
  List<String> get workspaces => failedBirths
@@ -489,8 +629,7 @@ class NomosEncryption {
489
629
 
490
630
  Future<List<String>> _readRoster(String scope) async {
491
631
  final s = _parseScope(scope);
492
- final rows =
493
- await bridge.queryById('reltuple-${s.object}-${s.relation}');
632
+ final rows = await bridge.queryById('reltuple-${s.object}-${s.relation}');
494
633
  final subjects = rows.isEmpty
495
634
  ? const <String, dynamic>{}
496
635
  : ((rows.first['data'] as Map?)?['subjects'] as Map?) ??
@@ -499,9 +638,8 @@ class NomosEncryption {
499
638
  subjects.forEach((subj, leafRaw) {
500
639
  Map? leaf;
501
640
  try {
502
- leaf = leafRaw is String
503
- ? jsonDecode(leafRaw) as Map
504
- : (leafRaw as Map);
641
+ leaf =
642
+ leafRaw is String ? jsonDecode(leafRaw) as Map : (leafRaw as Map);
505
643
  } catch (_) {
506
644
  leaf = null;
507
645
  }
@@ -524,11 +662,14 @@ class NomosEncryption {
524
662
  Future<String> _freshScopeKey() async {
525
663
  final kp = await bridge.cryptoKeygen();
526
664
  final secret = base64Decode((kp['secret'] ?? '').toString());
527
- return base64Encode(secret.sublist(0, 32)); // the 32-byte epoch secret; NEVER lands on chain
665
+ return base64Encode(secret.sublist(
666
+ 0, 32)); // the 32-byte epoch secret; NEVER lands on chain
528
667
  }
529
668
 
530
669
  Future<String?> _memberPubKey(String principal,
531
- {String? newcomer, String? newcomerPub, Map<String, String>? memberKeys}) async {
670
+ {String? newcomer,
671
+ String? newcomerPub,
672
+ Map<String, String>? memberKeys}) async {
532
673
  if (memberKeys != null && memberKeys[principal] != null) {
533
674
  return memberKeys[principal];
534
675
  }
@@ -589,7 +730,9 @@ class NomosEncryption {
589
730
  final wraps = <Map<String, Object?>>[];
590
731
  for (final principal in members) {
591
732
  final pub = await _memberPubKey(principal,
592
- newcomer: addPrincipal, newcomerPub: addPublicKey, memberKeys: memberKeys);
733
+ newcomer: addPrincipal,
734
+ newcomerPub: addPublicKey,
735
+ memberKeys: memberKeys);
593
736
  if (pub == null) throw EncryptionMemberKeyMissing(principal, scope);
594
737
  final w = await bridge.cryptoWrapKey(recipientPub: pub, scopeKey: key);
595
738
  wraps.add({
@@ -599,7 +742,8 @@ class NomosEncryption {
599
742
  'ct': (w['ct'] ?? '').toString(),
600
743
  });
601
744
  }
602
- final epoch = (await _currentEpoch(scope)) + 1; // forward-only: strictly increments
745
+ final epoch =
746
+ (await _currentEpoch(scope)) + 1; // forward-only: strictly increments
603
747
  final head = (await bridge.offerOutcome(
604
748
  domain: domain,
605
749
  directiveId: 'advanceKeyEpoch',
@@ -750,7 +894,8 @@ extension NomosRefAcquire on NomosRef {
750
894
  throw ArgumentError.value(this, 'ref',
751
895
  'NomosRef.acquire: the ref carries no workspace — only a workspace-complete ref is openable');
752
896
  }
753
- return bridge.acquireSession(workspace, ifAbsentCredentials: ifAbsentCredentials);
897
+ return bridge.acquireSession(workspace,
898
+ ifAbsentCredentials: ifAbsentCredentials);
754
899
  }
755
900
  }
756
901
 
@@ -817,9 +962,12 @@ class NomosBridge {
817
962
  final StreamController<TransferProgress> _transferEvents =
818
963
  StreamController<TransferProgress>.broadcast();
819
964
  // The DERIVED app-ready sync state. seeded-on-listen with a metrics poll; updated on sync/signal/transfer.
820
- late final StreamController<SyncStatus> _syncStatusCtrl = StreamController<SyncStatus>.broadcast(
965
+ late final StreamController<SyncStatus> _syncStatusCtrl =
966
+ StreamController<SyncStatus>.broadcast(
821
967
  onListen: _startStatusPoll,
822
- onCancel: () { if (!_syncStatusCtrl.hasListener) _stopStatusPoll(); },
968
+ onCancel: () {
969
+ if (!_syncStatusCtrl.hasListener) _stopStatusPoll();
970
+ },
823
971
  );
824
972
  SyncStatus _status = const SyncStatus();
825
973
  Timer? _metricsTimer;
@@ -852,7 +1000,8 @@ class NomosBridge {
852
1000
  StaleRunnerBundle? _staleBundleError(Object? bundleVersion) {
853
1001
  final v = bundleVersion is String ? bundleVersion : null;
854
1002
  if (v == 'unbundled-dev') return null;
855
- if (v != null && _semverAtLeast(v, kExpectedRunnerBundleVersion)) return null;
1003
+ if (v != null && _semverAtLeast(v, kExpectedRunnerBundleVersion))
1004
+ return null;
856
1005
  return StaleRunnerBundle(found: v, expected: kExpectedRunnerBundleVersion);
857
1006
  }
858
1007
 
@@ -963,7 +1112,8 @@ class NomosBridge {
963
1112
  if (event == 'lawChanged') {
964
1113
  // COMPAT SUPERPOWER (#72b): a synced install/upgrade/retire moved the workspace's offer under this
965
1114
  // live client. Cache the fresh verdict + emit it so the app re-checks feature flags without polling.
966
- final c = NomosCompat.fromJson((m['compat'] as Map?)?.cast<String, Object?>());
1115
+ final c =
1116
+ NomosCompat.fromJson((m['compat'] as Map?)?.cast<String, Object?>());
967
1117
  if (c != null) {
968
1118
  _lastCompat = c;
969
1119
  final sess = m['session'] as String?;
@@ -979,7 +1129,9 @@ class NomosBridge {
979
1129
  if (event == 'sync') {
980
1130
  // a completed round → idle (the next metrics poll folds in residual ahead/behind), stamp the time.
981
1131
  _pushStatus(_status.copyWith(
982
- phase: SyncPhase.idle, clearTransfer: true, lastSyncedAt: DateTime.now()));
1132
+ phase: SyncPhase.idle,
1133
+ clearTransfer: true,
1134
+ lastSyncedAt: DateTime.now()));
983
1135
  } else if (m['status'] == 'closed' || m['status'] == 'offline') {
984
1136
  _pushStatus(_status.copyWith(phase: SyncPhase.offline));
985
1137
  }
@@ -998,7 +1150,8 @@ class NomosBridge {
998
1150
  if (!_transferEvents.isClosed) _transferEvents.add(tp);
999
1151
  _pushStatus(_status.copyWith(
1000
1152
  phase: tp.done ? SyncPhase.syncing : SyncPhase.transferring,
1001
- transfer: tp, clearTransfer: tp.done));
1153
+ transfer: tp,
1154
+ clearTransfer: tp.done));
1002
1155
  return;
1003
1156
  }
1004
1157
  if (event == 'trace') {
@@ -1010,7 +1163,8 @@ class NomosBridge {
1010
1163
  // signal holon projection: ephemeral coordination such as peer frontiers / lag.
1011
1164
  if (!_signalEvents.isClosed) _signalEvents.add(m);
1012
1165
  // the doorbell says canonical main advanced → we're behind until the next pull lands.
1013
- if (_status.phase == SyncPhase.idle) _pushStatus(_status.copyWith(phase: SyncPhase.behind));
1166
+ if (_status.phase == SyncPhase.idle)
1167
+ _pushStatus(_status.copyWith(phase: SyncPhase.behind));
1014
1168
  return;
1015
1169
  }
1016
1170
  if (event == 'span') {
@@ -1087,7 +1241,8 @@ class NomosBridge {
1087
1241
  try {
1088
1242
  final parts = token.split('.');
1089
1243
  if (parts.length < 2) return null;
1090
- final payload = jsonDecode(utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))));
1244
+ final payload = jsonDecode(
1245
+ utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))));
1091
1246
  final sub = payload is Map ? payload['sub'] : null;
1092
1247
  if (sub is! String || sub.trim().isEmpty) return null;
1093
1248
  final value = sub.trim();
@@ -1115,7 +1270,8 @@ class NomosBridge {
1115
1270
  /// `ForeignNotFound`, not a throw) and reads it. [cloud] is the cloud the foreign workspaces sync from
1116
1271
  /// (usually the same cloud your primary workspace uses); omit for a purely local plane. This is a LOCATOR
1117
1272
  /// resolver, not a foreign key — the foreign holon is sovereign, mounted only when you choose to resolve.
1118
- NomosForeignReader foreignReader({String? cloud}) => _BridgeForeignReader(this, cloud);
1273
+ NomosForeignReader foreignReader({String? cloud}) =>
1274
+ _BridgeForeignReader(this, cloud);
1119
1275
 
1120
1276
  /// The workspace names [connect] has opened on this plane (the live sessions).
1121
1277
  List<String> get connectedSessions =>
@@ -1128,7 +1284,9 @@ class NomosBridge {
1128
1284
  final plane = _eventPlane;
1129
1285
  final scoped = _sessionName;
1130
1286
  if (scoped != null) return plane._sessionPrincipals[scoped];
1131
- return plane._sessionPrincipals.length == 1 ? plane._sessionPrincipals.values.first : null;
1287
+ return plane._sessionPrincipals.length == 1
1288
+ ? plane._sessionPrincipals.values.first
1289
+ : null;
1132
1290
  }
1133
1291
 
1134
1292
  /// ENUMERATE THE LOCAL PLANE — every workspace on this device + its lifecycle STATE and sync posture:
@@ -1141,17 +1299,17 @@ class NomosBridge {
1141
1299
  /// `syncPosture` is `"local-only"` (no cloud counterpart) or `"cloud-bound"`.
1142
1300
  Future<List<Map<String, Object?>>> listWorkspaces() async {
1143
1301
  final connected = _eventPlane._sessionNames.toSet();
1144
- return (((_asMap(await _call('listWorkspaces'))['workspaces']) as List?) ?? const [])
1302
+ return (((_asMap(await _call('listWorkspaces'))['workspaces']) as List?) ??
1303
+ const [])
1145
1304
  .map((e) => (e as Map).cast<String, Object?>())
1146
1305
  .map((w) {
1147
- final ws = w['workspace']?.toString() ?? '';
1148
- // `connected` is the app's own connect() truth; `parked`/resident come from the runner realm.
1149
- w['state'] = (w['parked'] == true)
1150
- ? 'parked'
1151
- : (connected.contains(ws) ? 'connected' : 'resident');
1152
- return w;
1153
- })
1154
- .toList();
1306
+ final ws = w['workspace']?.toString() ?? '';
1307
+ // `connected` is the app's own connect() truth; `parked`/resident come from the runner realm.
1308
+ w['state'] = (w['parked'] == true)
1309
+ ? 'parked'
1310
+ : (connected.contains(ws) ? 'connected' : 'resident');
1311
+ return w;
1312
+ }).toList();
1155
1313
  }
1156
1314
 
1157
1315
  /// Open a workspace on this plane: boot its LOCAL githolon under the installed law (from [restoreFrom]
@@ -1200,22 +1358,26 @@ class NomosBridge {
1200
1358
  // fail closed (the break surfaces as an error on the sync/pull that hit it; the holon stays local).
1201
1359
  if (onCustodyBreak != null) 'onCustodyBreak': onCustodyBreak.wire,
1202
1360
  }));
1203
- _eventPlane._sessionNames.add(ws); // the open succeeded — the session is real now
1361
+ _eventPlane._sessionNames
1362
+ .add(ws); // the open succeeded — the session is real now
1204
1363
  final boundPrincipal = _principalFromToken(authToken);
1205
- if (boundPrincipal != null) _eventPlane._sessionPrincipals[ws] = boundPrincipal;
1206
- _eventPlane._lastRecovery = (r['recovery'] as Map?)?.cast<String, Object?>();
1364
+ if (boundPrincipal != null)
1365
+ _eventPlane._sessionPrincipals[ws] = boundPrincipal;
1366
+ _eventPlane._lastRecovery =
1367
+ (r['recovery'] as Map?)?.cast<String, Object?>();
1207
1368
  }
1208
1369
 
1209
1370
  /// The custody lifecycle stream (rebirth_and_custody_lifecycle.md) — a rebirth/divergence surfaces here as a
1210
1371
  /// typed [NomosCustodyEvent]. Bind a Flutter break dialog / posture banner to it; for a `CustodyBreakPolicy.
1211
1372
  /// ask` session, an event of `kind == 'ask'` awaits [resolveCustodyBreak].
1212
- Stream<NomosCustodyEvent> get custodyEvents => _eventPlane._custodyEvents.stream;
1373
+ Stream<NomosCustodyEvent> get custodyEvents =>
1374
+ _eventPlane._custodyEvents.stream;
1213
1375
 
1214
1376
  /// Where a session sits on the custody spectrum (incarnation, posture, pending/salvaged local work,
1215
1377
  /// supersession history) — for a status banner or a "you're offline on old truth" indicator.
1216
1378
  Future<NomosCustodyStatus> custodyStatus({String? session}) async =>
1217
- NomosCustodyStatus.fromJson(
1218
- _asMap(await _call('custodyStatus', {if (session != null) 'session': session})));
1379
+ NomosCustodyStatus.fromJson(_asMap(await _call(
1380
+ 'custodyStatus', {if (session != null) 'session': session})));
1219
1381
 
1220
1382
  /// Continue an `ask`-policy custody break with the user's decision (adopt|reject) — the async UX lane.
1221
1383
  Future<Map<String, Object?>> resolveCustodyBreak(String decision,
@@ -1227,7 +1389,8 @@ class NomosBridge {
1227
1389
 
1228
1390
  /// The parked salvaged intents (re-offerable local work an `adopt` rescued) — metadata only.
1229
1391
  Future<List<Map<String, Object?>>> salvagedIntents({String? session}) async {
1230
- final r = _asMap(await _call('salvagedIntents', {if (session != null) 'session': session}));
1392
+ final r = _asMap(await _call(
1393
+ 'salvagedIntents', {if (session != null) 'session': session}));
1231
1394
  return ((r['salvaged'] as List?) ?? const [])
1232
1395
  .map((e) => (e as Map).cast<String, Object?>())
1233
1396
  .toList();
@@ -1235,13 +1398,16 @@ class NomosBridge {
1235
1398
 
1236
1399
  /// Re-offer parked salvaged work under the current (adopted) law — the explicit app move (never automatic
1237
1400
  /// across incarnations). [id] re-offers one; omitted re-offers all.
1238
- Future<Map<String, Object?>> reofferSalvaged({String? id, String? session}) async =>
1401
+ Future<Map<String, Object?>> reofferSalvaged(
1402
+ {String? id, String? session}) async =>
1239
1403
  _asMap(await _call('reofferSalvaged',
1240
1404
  {if (id != null) 'id': id, if (session != null) 'session': session}));
1241
1405
 
1242
1406
  /// Discard one parked salvaged intent — the app's explicit choice.
1243
- Future<Map<String, Object?>> discardSalvaged(String id, {String? session}) async =>
1244
- _asMap(await _call('discardSalvaged', {'id': id, if (session != null) 'session': session}));
1407
+ Future<Map<String, Object?>> discardSalvaged(String id,
1408
+ {String? session}) async =>
1409
+ _asMap(await _call('discardSalvaged',
1410
+ {'id': id, if (session != null) 'session': session}));
1245
1411
 
1246
1412
  // ── THE DEAD-LETTER QUEUE (refused-but-legitimate work is NEVER lost) — the Dart mirror of the
1247
1413
  // JS holon's DLQ surface (`deadLetters()` / `retryDeadLetter()` / `discardDeadLetter()`). A
@@ -1252,7 +1418,8 @@ class NomosBridge {
1252
1418
  /// The parked dead letters — metadata only (`{id, oid, source, error, domain, directiveId, at}`);
1253
1419
  /// the intent bytes stay in the durable queue.
1254
1420
  Future<List<Map<String, Object?>>> deadLetters({String? session}) async {
1255
- final r = _asMap(await _call('deadLetters', {if (session != null) 'session': session}));
1421
+ final r = _asMap(
1422
+ await _call('deadLetters', {if (session != null) 'session': session}));
1256
1423
  return ((r['dead'] as List?) ?? const [])
1257
1424
  .map((e) => (e as Map).cast<String, Object?>())
1258
1425
  .toList();
@@ -1261,12 +1428,16 @@ class NomosBridge {
1261
1428
  /// Retry one dead letter (after the law fix landed): pulls canon first (it may already be
1262
1429
  /// admitted), else re-offers the parked bytes locally. `{ok, head?|alreadyOnMain?|error?}` —
1263
1430
  /// a refusal leaves the entry parked (with the fresh error), never lost.
1264
- Future<Map<String, Object?>> retryDeadLetter(String id, {String? session}) async =>
1265
- _asMap(await _call('retryDeadLetter', {'id': id, if (session != null) 'session': session}));
1431
+ Future<Map<String, Object?>> retryDeadLetter(String id,
1432
+ {String? session}) async =>
1433
+ _asMap(await _call('retryDeadLetter',
1434
+ {'id': id, if (session != null) 'session': session}));
1266
1435
 
1267
1436
  /// Discard one dead letter — the app's EXPLICIT choice to drop the work. `{ok}`.
1268
- Future<Map<String, Object?>> discardDeadLetter(String id, {String? session}) async =>
1269
- _asMap(await _call('discardDeadLetter', {'id': id, if (session != null) 'session': session}));
1437
+ Future<Map<String, Object?>> discardDeadLetter(String id,
1438
+ {String? session}) async =>
1439
+ _asMap(await _call('discardDeadLetter',
1440
+ {'id': id, if (session != null) 'session': session}));
1270
1441
 
1271
1442
  /// SALVAGE — best-effort extraction of this holon's local-only authored work (un-synced pending
1272
1443
  /// writes + the DLQ), each with its full `intentB64` bytes: `{pendingWrites, deadLetters}`. The
@@ -1278,9 +1449,9 @@ class NomosBridge {
1278
1449
  /// (a refusal dead-letters — never lost), and dead-letter entries are re-queued into the DLQ.
1279
1450
  /// Returns the recovery report `{salvagedWrites, requeuedDeadLetters, unsalvageable}`.
1280
1451
  Future<Map<String, Object?>> absorbSalvage(
1281
- {List<Map<String, Object?>> pendingWrites = const [],
1282
- List<Map<String, Object?>> deadLetters = const [],
1283
- String? session}) async =>
1452
+ {List<Map<String, Object?>> pendingWrites = const [],
1453
+ List<Map<String, Object?>> deadLetters = const [],
1454
+ String? session}) async =>
1284
1455
  _asMap(await _call('absorbSalvage', {
1285
1456
  'pendingWrites': pendingWrites,
1286
1457
  'deadLetters': deadLetters,
@@ -1294,7 +1465,8 @@ class NomosBridge {
1294
1465
  Map<String, Object?>? _lastRecovery;
1295
1466
 
1296
1467
  /// LOCAL-FIRST offer: carry opaque intent bytes into the GitHolon. Returns the new local head; durable on [sync].
1297
- @Deprecated('Engine-level generic writes are the footgun (session_bound_clients.md ask 1/7): '
1468
+ @Deprecated(
1469
+ 'Engine-level generic writes are the footgun (session_bound_clients.md ask 1/7): '
1298
1470
  'dispatch through a generated <Domain>Client bound to a NomosSession '
1299
1471
  '(XClient.bind(session)). Retained for CLIs/admin tooling/generated internals only.')
1300
1472
  Future<String> offer({required String intentBytes}) async {
@@ -1312,7 +1484,8 @@ class NomosBridge {
1312
1484
  /// its trust policy); [force] carries the deploy/evolve force flag through. Both are OMITTED from the wire
1313
1485
  /// when absent, so a plain offer is byte-identical to before. [session] pins the colocated workspace in a
1314
1486
  /// cluster harness (absent ⇒ the runner's sole-session default).
1315
- @Deprecated('Engine-level string-y writes are the footgun (session_bound_clients.md ask 1/7): '
1487
+ @Deprecated(
1488
+ 'Engine-level string-y writes are the footgun (session_bound_clients.md ask 1/7): '
1316
1489
  'dispatch through a generated <Domain>Client bound to a NomosSession '
1317
1490
  '(XClient.bind(session) — no domain/directive strings at call sites). '
1318
1491
  'Retained for CLIs/admin tooling/generated internals only.')
@@ -1323,6 +1496,7 @@ class NomosBridge {
1323
1496
  String? actor,
1324
1497
  String? domainHash,
1325
1498
  List<Map<String, Object?>>? attestations,
1499
+ List<Map<String, Object?>>? nomosConflictSnapshots,
1326
1500
  bool? force,
1327
1501
  String? session,
1328
1502
  // Per-offer signing key (base64 device secret): sign THIS offer with a specific device key instead of
@@ -1336,6 +1510,8 @@ class NomosBridge {
1336
1510
  if (actor != null) 'actor': actor,
1337
1511
  if (domainHash != null) 'domainHash': domainHash,
1338
1512
  if (attestations != null) 'attestations': attestations,
1513
+ if (nomosConflictSnapshots != null)
1514
+ '__nomosConflictSnapshots': nomosConflictSnapshots,
1339
1515
  if (force != null) 'force': force,
1340
1516
  if (session != null) 'session': session,
1341
1517
  if (authorSecret != null) 'authorSecret': authorSecret,
@@ -1347,7 +1523,8 @@ class NomosBridge {
1347
1523
  /// inside the sealed intent (callers never supply one); the runner hands back what was minted, so a
1348
1524
  /// generated create method returns the typed id with NO query-after round-trip. [created] is empty for
1349
1525
  /// pure mutations.
1350
- @Deprecated('Engine-level string-y writes are the footgun (session_bound_clients.md ask 1/7): '
1526
+ @Deprecated(
1527
+ 'Engine-level string-y writes are the footgun (session_bound_clients.md ask 1/7): '
1351
1528
  'dispatch through a generated <Domain>Client bound to a NomosSession. '
1352
1529
  'Retained for CLIs/admin tooling/generated internals only.')
1353
1530
  Future<NomosOfferOutcome> offerCreates({
@@ -1357,6 +1534,7 @@ class NomosBridge {
1357
1534
  String? actor,
1358
1535
  String? domainHash,
1359
1536
  List<Map<String, Object?>>? attestations,
1537
+ List<Map<String, Object?>>? nomosConflictSnapshots,
1360
1538
  bool? force,
1361
1539
  String? session,
1362
1540
  String? authorSecret,
@@ -1368,6 +1546,7 @@ class NomosBridge {
1368
1546
  actor: actor,
1369
1547
  domainHash: domainHash,
1370
1548
  attestations: attestations,
1549
+ nomosConflictSnapshots: nomosConflictSnapshots,
1371
1550
  force: force,
1372
1551
  session: session,
1373
1552
  authorSecret: authorSecret,
@@ -1385,6 +1564,7 @@ class NomosBridge {
1385
1564
  String? actor,
1386
1565
  String? domainHash,
1387
1566
  List<Map<String, Object?>>? attestations,
1567
+ List<Map<String, Object?>>? nomosConflictSnapshots,
1388
1568
  bool? force,
1389
1569
  String? session,
1390
1570
  // Per-offer signing key (base64 device secret): sign THIS offer with a specific device key instead
@@ -1407,6 +1587,8 @@ class NomosBridge {
1407
1587
  if (actor != null) 'actor': actor,
1408
1588
  if (domainHash != null) 'domainHash': domainHash,
1409
1589
  if (attestations != null) 'attestations': attestations,
1590
+ if (nomosConflictSnapshots != null)
1591
+ '__nomosConflictSnapshots': nomosConflictSnapshots,
1410
1592
  if (force != null) 'force': force,
1411
1593
  if (session != null) 'session': session,
1412
1594
  if (authorSecret != null) 'authorSecret': authorSecret,
@@ -1424,12 +1606,19 @@ class NomosBridge {
1424
1606
  final x = v.toInt();
1425
1607
  return x < t0 ? t0 : (x > t1 ? t1 : x);
1426
1608
  }
1609
+
1427
1610
  noteLocalSpan(
1428
- track: 'submit', name: 'offer: bridge ipc (up-leg)', kind: 'bridge-ipc',
1429
- t0: t0, t1: within(recv));
1611
+ track: 'submit',
1612
+ name: 'offer: bridge ipc (up-leg)',
1613
+ kind: 'bridge-ipc',
1614
+ t0: t0,
1615
+ t1: within(recv));
1430
1616
  noteLocalSpan(
1431
- track: 'submit', name: 'offer: response delivery (down-leg)', kind: 'response-delivery',
1432
- t0: within(done), t1: t1);
1617
+ track: 'submit',
1618
+ name: 'offer: response delivery (down-leg)',
1619
+ kind: 'response-delivery',
1620
+ t0: within(done),
1621
+ t1: t1);
1433
1622
  }
1434
1623
  }
1435
1624
  final births = ((r['birthOutcomes'] as List?) ?? const [])
@@ -1451,8 +1640,8 @@ class NomosBridge {
1451
1640
  final bornRaw = (r['born'] as List?);
1452
1641
  final bornNames = <String>[
1453
1642
  if (bornRaw != null)
1454
- ...bornRaw.map((e) =>
1455
- (e is Map ? (e['workspace'] ?? '') : e).toString())
1643
+ ...bornRaw
1644
+ .map((e) => (e is Map ? (e['workspace'] ?? '') : e).toString())
1456
1645
  else
1457
1646
  ...births
1458
1647
  .map((m) => (m['workspace'] ?? '').toString())
@@ -1487,15 +1676,20 @@ class NomosBridge {
1487
1676
  /// originates in the sealed kernel, so the bridge (not the kernel) owns the ergonomics.
1488
1677
  /// Anything that isn't that refusal passes through unchanged.
1489
1678
  Future<Object> _wrapDomainNotInstalled(Object e,
1490
- {required String domain, String? domainHash, String? requestedWorkspace}) async {
1491
- if (!RegExp(r"no active installed law declares domain '").hasMatch(e.toString())) return e;
1679
+ {required String domain,
1680
+ String? domainHash,
1681
+ String? requestedWorkspace}) async {
1682
+ if (!RegExp(r"no active installed law declares domain '")
1683
+ .hasMatch(e.toString())) return e;
1492
1684
  final self = this;
1493
1685
  final sessionWs = self is NomosSession ? self.workspace : null;
1494
1686
  var activeKeys = const <String>[];
1495
1687
  try {
1496
1688
  final offer = await interfaceOffer();
1497
1689
  if (offer != null) activeKeys = offer.keys.toList()..sort();
1498
- } catch (_) {/* best-effort enrichment — the typed error still carries the context we have */}
1690
+ } catch (_) {
1691
+ /* best-effort enrichment — the typed error still carries the context we have */
1692
+ }
1499
1693
  return DomainNotInstalledForSession(
1500
1694
  expectedDomainKey: domain,
1501
1695
  expectedDomainHash: domainHash,
@@ -1512,7 +1706,8 @@ class NomosBridge {
1512
1706
  /// installed package, read off the LOCAL projection (offline). Keys = the ACTIVE domain keys the
1513
1707
  /// workspace serves. Null when no on-ledger interface is observed yet (pre-#72e law).
1514
1708
  Future<Map<String, Object?>?> interfaceOffer({String? session}) async {
1515
- final r = _asMap(await _call('interfaceOffer', {if (session != null) 'session': session}));
1709
+ final r = _asMap(await _call(
1710
+ 'interfaceOffer', {if (session != null) 'session': session}));
1516
1711
  return (r['offer'] as Map?)?.cast<String, Object?>();
1517
1712
  }
1518
1713
 
@@ -1525,7 +1720,11 @@ class NomosBridge {
1525
1720
  /// [authorSecret] — the device signing key for the followed session's writes (era-0: an attested-born
1526
1721
  /// child is WARRANTED; write access needs its owner's enrolled device key).
1527
1722
  Future<NomosSession> follow(NomosRef ref,
1528
- {String? cloud, String? clientId, String? authToken, bool allowUnborn = false, String? authorSecret}) async {
1723
+ {String? cloud,
1724
+ String? clientId,
1725
+ String? authToken,
1726
+ bool allowUnborn = false,
1727
+ String? authorSecret}) async {
1529
1728
  final ws = ref.workspace;
1530
1729
  if (ws.isEmpty) {
1531
1730
  throw ArgumentError.value(ref, 'ref',
@@ -1580,7 +1779,11 @@ class NomosBridge {
1580
1779
  // a pre-connect plane has no registry yet — fall through to the credentials decision below.
1581
1780
  }
1582
1781
  if (resident) {
1583
- await connect(workspace: workspace, session: workspace, allowUnborn: true, authorSecret: authorSecret);
1782
+ await connect(
1783
+ workspace: workspace,
1784
+ session: workspace,
1785
+ allowUnborn: true,
1786
+ authorSecret: authorSecret);
1584
1787
  return session(workspace);
1585
1788
  }
1586
1789
  if (ifAbsentCredentials == null) {
@@ -1621,7 +1824,8 @@ class NomosBridge {
1621
1824
  /// OFFER AN INTENT — the write verb. Plan a directive (domain/directiveId/payload) as the verified [actor];
1622
1825
  /// the holon encodes the v2 offer envelope via the single codec and admits it. Returns the new local head.
1623
1826
  /// (This is the canonical name; [offerDirective] is the legacy alias the generated clients still call.)
1624
- @Deprecated('Engine-level string-y writes are the footgun (session_bound_clients.md ask 1/7): '
1827
+ @Deprecated(
1828
+ 'Engine-level string-y writes are the footgun (session_bound_clients.md ask 1/7): '
1625
1829
  'dispatch through a generated <Domain>Client bound to a NomosSession. '
1626
1830
  'Retained for CLIs/admin tooling/generated internals only.')
1627
1831
  Future<String> offerIntent({
@@ -1631,6 +1835,7 @@ class NomosBridge {
1631
1835
  String? actor,
1632
1836
  String? domainHash,
1633
1837
  List<Map<String, Object?>>? attestations,
1838
+ List<Map<String, Object?>>? nomosConflictSnapshots,
1634
1839
  bool? force,
1635
1840
  String? session,
1636
1841
  }) =>
@@ -1641,6 +1846,7 @@ class NomosBridge {
1641
1846
  actor: actor,
1642
1847
  domainHash: domainHash,
1643
1848
  attestations: attestations,
1849
+ nomosConflictSnapshots: nomosConflictSnapshots,
1644
1850
  force: force,
1645
1851
  session: session);
1646
1852
 
@@ -1698,7 +1904,8 @@ class NomosBridge {
1698
1904
  /// `bridge.createWorkspace(domain: 'co2_platform', directiveId: 'birthEstateWorkspace', domainHash: ...)`.
1699
1905
  /// The kernel looks up `(domain, directiveId)` in the ACTIVE installed law — [domain] must name the law
1700
1906
  /// that actually declares [directiveId], never assume `'workspaces'`.
1701
- @Deprecated('Engine-level generic writes with a loose parent string are the footgun '
1907
+ @Deprecated(
1908
+ 'Engine-level generic writes with a loose parent string are the footgun '
1702
1909
  '(session_bound_clients.md asks 1/4/7): births go through the generated <Domain>Client '
1703
1910
  'bound to the PARENT NomosSession (its birth method infers parent = the session workspace). '
1704
1911
  'Retained for CLIs/admin tooling/generated compatibility shims only.')
@@ -1807,6 +2014,59 @@ class NomosBridge {
1807
2014
  'directiveId': directiveId,
1808
2015
  }));
1809
2016
 
2017
+ /// Framework-private business correction seam. Generated clients bind the domain, its actions and actor;
2018
+ /// application code supplies only the correction kind and its mandatory reason.
2019
+ Future<Map<String, Object?>> correctMostRecent({
2020
+ required String kind,
2021
+ required String domain,
2022
+ required List<String> actions,
2023
+ required String reason,
2024
+ String? domainHash,
2025
+ String? actor,
2026
+ }) async =>
2027
+ _asMap(await _call('correctMostRecent', {
2028
+ 'kind': kind,
2029
+ 'domain': domain,
2030
+ 'actions': actions,
2031
+ 'reason': reason,
2032
+ if (domainHash != null) 'domainHash': domainHash,
2033
+ if (actor != null) 'actor': actor,
2034
+ }));
2035
+
2036
+ /// Framework-private exact-history adoption seam. Generated clients expose this as
2037
+ /// `model.adopt()` and, only when prior business outcomes differ, a chained
2038
+ /// `decision.accept(reason)`. Candidate identities, heads and receipts stay here.
2039
+ Future<Map<String, Object?>> nomosFrameworkAdoptGeneratedModel({
2040
+ required String domain,
2041
+ required String candidateHash,
2042
+ String? because,
2043
+ }) async {
2044
+ final r = _asMap(await _call('adoptGeneratedModel', {
2045
+ 'domain': domain,
2046
+ 'candidateHash': candidateHash,
2047
+ if (because != null) 'because': because,
2048
+ }));
2049
+ return ((r['verdict'] as Map?) ?? const <String, Object?>{})
2050
+ .cast<String, Object?>();
2051
+ }
2052
+
2053
+ /// Framework-private exact-history restoration seam. Generated clients expose
2054
+ /// `model.restorePrevious()` and a chained `decision.accept(reason)`; predecessor
2055
+ /// identity, derived dispositions, heads and receipts stay below this bridge.
2056
+ Future<Map<String, Object?>> nomosFrameworkRestorePreviousGeneratedModel({
2057
+ required String domain,
2058
+ required String currentHash,
2059
+ String? because,
2060
+ }) async {
2061
+ final r = _asMap(await _call('restorePreviousGeneratedModel', {
2062
+ 'domain': domain,
2063
+ 'currentHash': currentHash,
2064
+ if (because != null) 'because': because,
2065
+ }));
2066
+ return ((r['verdict'] as Map?) ?? const <String, Object?>{})
2067
+ .cast<String, Object?>();
2068
+ }
2069
+
1810
2070
  /// HISTORY of one aggregate (a todo) — the full action timeline from the ledger: every intent that touched
1811
2071
  /// it + the strikes that retract them (undo/restore/redo), oldest→newest. Each entry:
1812
2072
  /// `{id, who, mine, at(ms), action, fields[], done?, text?, removed?, marker, isStrike, targetAction, struck}`.
@@ -1820,8 +2080,9 @@ class NomosBridge {
1820
2080
  }
1821
2081
 
1822
2082
  /// RUNTIME activity feed — per-committed-intent rows (newest-first) from the local ledger, each carrying
1823
- /// `{intentId, domain, directiveId, actorSubject, occurredAt, payload}` (payload = the DECODED directive
1824
- /// payload). `base` = a commit-oid cursor (only intents newer than it); omitted = from the tip bounded by
2083
+ /// `{intentId, domain, directiveId, actorSubject, occurredAt, payload, created, affected}` (payload = the
2084
+ /// DECODED directive payload; affected = the aggregate refs its sealed events touched). `base` = a commit-
2085
+ /// oid cursor (only intents newer than it); omitted = from the tip bounded by
1825
2086
  /// `limit` (default 200). Generic raw rows — the typed shaping is the codegen `renderActivity`.
1826
2087
  Future<List<Map<String, dynamic>>> activityLog(
1827
2088
  {String? base, int? limit}) async {
@@ -1834,6 +2095,15 @@ class NomosBridge {
1834
2095
  .toList();
1835
2096
  }
1836
2097
 
2098
+ /// Framework-private organisational read used by generated `describe()`. The runner joins the parent
2099
+ /// ledger's verified birth witnesses and lifecycle facts; tenant code only receives safe child links.
2100
+ Future<List<Map<String, dynamic>>> nomosFrameworkChildWorkspaces() async {
2101
+ final r = _asMap(await _call('nomosChildWorkspaces'));
2102
+ return ((r['children'] as List?) ?? const [])
2103
+ .map((e) => (e as Map).cast<String, dynamic>())
2104
+ .toList();
2105
+ }
2106
+
1837
2107
  /// Watch the RUNTIME activity feed: a stream that re-emits the newest-first activity rows on every local
1838
2108
  /// change (doorbell/sync tick). `base` = a commit-oid cursor. Cancelling drops the watch on the runner.
1839
2109
  Stream<List<dynamic>> watchActivity({String? base, String? session}) =>
@@ -1897,7 +2167,8 @@ class NomosBridge {
1897
2167
  /// (the secret never leaves the device). The recovery lanes use this when only the secret was
1898
2168
  /// persisted (the device-key store keeps just the seed), including low-level device-bound enrollment.
1899
2169
  Future<String> devicePublicOf(String secret) async =>
1900
- (_asMap(await _call('devicePublicOf', {'secret': secret}))['public'] ?? '')
2170
+ (_asMap(await _call('devicePublicOf', {'secret': secret}))['public'] ??
2171
+ '')
1901
2172
  .toString();
1902
2173
 
1903
2174
  /// PARENT-AUTHORITY POSTURE (read-only diagnostic): is this device key a LIVE `signer` for
@@ -1986,8 +2257,9 @@ class NomosBridge {
1986
2257
  /// kernel `cert_sign` op (the single source of the byte layout), OFFER the PARAMETERIZED parent's `birthHome`
1987
2258
  /// directive carrying the homeCert, poll the parent for the born home, and CONFIRM the born home's own
1988
2259
  /// `verify_chain` is GREEN (the offline peer-verify — replay IS validation). `spec` MUST carry `parent` (the
1989
- /// tenant's platform whose governance includes `birthHome`), `principal`, `frameworkHash`, and `lawHash`;
1990
- /// `subject`/`authToken`/`directiveId` are optional. OURS is the ceremony; the parent is the TENANT's.
2260
+ /// tenant's platform whose governance includes `birthHome`) and `principal`; the client discovers the
2261
+ /// private framework route and active law hashes from that application's live contract. `subject`/
2262
+ /// `authToken`/`directiveId` are optional. OURS is the ceremony; the parent is the TENANT's.
1991
2263
  /// Returns `{home, device:{secret,public}, homeCert, born, verify:{valid,…}}`.
1992
2264
  Future<Map<String, Object?>> birthHome(Map<String, Object?> spec) async =>
1993
2265
  _asMap(await _call('birthHome', spec));
@@ -2045,19 +2317,84 @@ class NomosBridge {
2045
2317
 
2046
2318
  /// A single aggregate by id (the by-id read). `keys`/`principal`: see [query].
2047
2319
  Future<List<Map<String, dynamic>>> queryById(String id,
2048
- [List<NomosScopeKey>? keys, String? principal, String? session]) async =>
2320
+ [List<NomosScopeKey>? keys,
2321
+ String? principal,
2322
+ String? session]) async =>
2049
2323
  _asRows(await _call('queryById', {
2050
2324
  'id': id,
2051
2325
  ...?_readAuthArgs(keys, principal),
2052
2326
  if (session != null) 'session': session,
2053
2327
  }));
2054
2328
 
2329
+ /// Verified points in this workspace's history, oldest to newest. The generated client presents these
2330
+ /// as `client.past.moments()`; callers pass the opaque [NomosMoment] back to `past.at(moment)` and never
2331
+ /// need to know that the framework uses commit identities underneath.
2332
+ Future<List<NomosMoment>> moments({int limit = 100, String? session}) async {
2333
+ final r = _asMap(await _call('commits', {
2334
+ 'limit': limit,
2335
+ if (session != null) 'session': session,
2336
+ }));
2337
+ return ((r['commits'] as List?) ?? const [])
2338
+ .map((e) => NomosMoment.fromJson((e as Map).cast<String, Object?>()))
2339
+ .toList();
2340
+ }
2341
+
2342
+ /// Run one declared business query against the verified world at [moment], without moving the live world.
2343
+ /// `keys`/`principal` have exactly the same private/encrypted-read semantics as [query].
2344
+ Future<List<Map<String, dynamic>>> queryAt(NomosMoment moment, String queryId,
2345
+ [Map<String, Object?> params = const {},
2346
+ List<NomosScopeKey>? keys,
2347
+ String? principal,
2348
+ String? session]) async =>
2349
+ _asRows(await _call('queryAt', {
2350
+ 'commit': moment.id,
2351
+ 'queryId': queryId,
2352
+ 'params': params,
2353
+ ...?_readAuthArgs(keys, principal),
2354
+ if (session != null) 'session': session,
2355
+ }));
2356
+
2357
+ /// Read one aggregate from the verified world at [moment]. `keys`/`principal`: see [queryAt].
2358
+ Future<List<Map<String, dynamic>>> queryByIdAt(NomosMoment moment, String id,
2359
+ [List<NomosScopeKey>? keys,
2360
+ String? principal,
2361
+ String? session]) async =>
2362
+ _asRows(await _call('queryByIdAt', {
2363
+ 'commit': moment.id,
2364
+ 'id': id,
2365
+ ...?_readAuthArgs(keys, principal),
2366
+ if (session != null) 'session': session,
2367
+ }));
2368
+
2369
+ /// Maintained historic count/sum values at [moment]. These are the temporal counterparts of [count]/[sum].
2370
+ Future<int> countAt(NomosMoment moment, String countId,
2371
+ [String group = '', String? session]) async =>
2372
+ ((_asMap(await _call('countAt', {
2373
+ 'commit': moment.id,
2374
+ 'countId': countId,
2375
+ 'group': group,
2376
+ if (session != null) 'session': session,
2377
+ }))['count']) as num)
2378
+ .toInt();
2379
+
2380
+ Future<int> sumAt(NomosMoment moment, String sumId,
2381
+ [String group = '', String? session]) async =>
2382
+ ((_asMap(await _call('sumAt', {
2383
+ 'commit': moment.id,
2384
+ 'sumId': sumId,
2385
+ 'group': group,
2386
+ if (session != null) 'session': session,
2387
+ }))['sum']) as num)
2388
+ .toInt();
2389
+
2055
2390
  /// The read auth sidecar (private reads + E2E): `keys`/`principal` as top-level call args, or null when
2056
2391
  /// neither is present (so the wire is byte-identical to before this existed).
2057
- Map<String, Object?>? _readAuthArgs(List<NomosScopeKey>? keys, String? principal) {
2392
+ Map<String, Object?>? _readAuthArgs(
2393
+ List<NomosScopeKey>? keys, String? principal) {
2058
2394
  final out = <String, Object?>{};
2059
2395
  if (principal != null && principal.isNotEmpty) out['principal'] = principal;
2060
- if (keys != null && keys.isNotEmpty) out['keys'] = keys.map((k) => k.toJson()).toList();
2396
+ if (keys != null && keys.isNotEmpty)
2397
+ out['keys'] = keys.map((k) => k.toJson()).toList();
2061
2398
  return out.isEmpty ? null : out;
2062
2399
  }
2063
2400
 
@@ -2078,15 +2415,127 @@ class NomosBridge {
2078
2415
  required String fieldSid,
2079
2416
  required String plaintext}) async =>
2080
2417
  (_asMap(await _call('cryptoSealField', {
2081
- 'scopeKey': scopeKey,
2418
+ 'scopeKey': scopeKey,
2419
+ 'scope': scope,
2420
+ 'epoch': epoch,
2421
+ 'aggSid': aggSid,
2422
+ 'fieldSid': fieldSid,
2423
+ 'plaintext': plaintext,
2424
+ }))['envelope'] ??
2425
+ '')
2426
+ .toString();
2427
+
2428
+ /// FRAMEWORK-INTERNAL `.owned()` key lane. The connected JS holon derives epoch 0 from its
2429
+ /// already-persisted device credential; generated clients call this without exposing key custody to apps.
2430
+ Future<NomosScopeKey> nomosOwnedScopeKey(String scope,
2431
+ {String? recordType, String? recordId}) async {
2432
+ final out = _asMap(await _call('cryptoOwnedScopeKey', {
2433
+ 'scope': scope,
2434
+ if (recordType != null && recordId != null)
2435
+ 'record': {'type': recordType, 'id': recordId},
2436
+ }));
2437
+ return NomosScopeKey(
2438
+ scope: (out['scope'] ?? scope).toString(),
2439
+ epoch: (out['epoch'] as num?)?.toInt() ?? 0,
2440
+ key: (out['key'] ?? '').toString(),
2441
+ ownedKeySubject: out['ownedKeySubject']?.toString(),
2442
+ ownedCustody: out['ownedCustody']?.toString(),
2443
+ );
2444
+ }
2445
+
2446
+ /// Every key epoch this principal currently holds for one owned record. Generated reads use the
2447
+ /// complete set so history remains readable after rotations, shares and transfers.
2448
+ Future<List<NomosScopeKey>> nomosOwnedScopeKeys(String scope,
2449
+ {required String recordType, required String recordId}) async {
2450
+ final out = _asMap(await _call('cryptoOwnedScopeKeys', {
2451
+ 'scope': scope,
2452
+ 'record': {'type': recordType, 'id': recordId},
2453
+ }));
2454
+ return (out['keys'] as List? ?? const [])
2455
+ .map((raw) => _asMap(raw))
2456
+ .map((key) => NomosScopeKey(
2457
+ scope: (key['scope'] ?? scope).toString(),
2458
+ epoch: (key['epoch'] as num?)?.toInt() ?? 0,
2459
+ key: (key['key'] ?? '').toString(),
2460
+ ownedKeySubject: key['ownedKeySubject']?.toString(),
2461
+ ownedCustody: key['ownedCustody']?.toString(),
2462
+ ))
2463
+ .toList();
2464
+ }
2465
+
2466
+ /// Make this signed-in principal ready to receive record shares. Generated clients call this while
2467
+ /// binding; tenant code never handles a key, certificate or identity tuple.
2468
+ Future<void> nomosEnsureOwnedIdentity(
2469
+ {required String scope,
2470
+ required String domain,
2471
+ String? domainHash}) async {
2472
+ await _call('cryptoOwnedEnsureIdentity', {
2473
+ 'scope': scope,
2474
+ 'domain': domain,
2475
+ if (domainHash != null) 'domainHash': domainHash,
2476
+ });
2477
+ }
2478
+
2479
+ /// Opt into designated-peer all-device-loss recovery. All protocol fields remain in the JS/kernel client.
2480
+ Future<Map<String, Object?>> nomosProtectOwnedIdentity({
2481
+ required String scope,
2482
+ required String domain,
2483
+ required String domainHash,
2484
+ required String recoveryPrincipal,
2485
+ }) async =>
2486
+ _asMap(await _call('cryptoOwnedProtectIdentity', {
2082
2487
  'scope': scope,
2083
- 'epoch': epoch,
2084
- 'aggSid': aggSid,
2085
- 'fieldSid': fieldSid,
2086
- 'plaintext': plaintext,
2087
- }))['envelope'] ?? '')
2488
+ 'domain': domain,
2489
+ 'domainHash': domainHash,
2490
+ 'recoveryPrincipal': recoveryPrincipal,
2491
+ }));
2492
+
2493
+ /// Prepare the private framework payload behind generated share/unshare/transfer methods.
2494
+ Future<Map<String, Object?>> nomosPrepareOwnedRecordAction(
2495
+ {required String action,
2496
+ required String scope,
2497
+ required NomosRef record,
2498
+ required String peer}) async =>
2499
+ _asMap(await _call('cryptoOwnedPrepareRecordAction', {
2500
+ 'action': action,
2501
+ 'scope': scope,
2502
+ 'record': {'type': record.type, 'id': record.id},
2503
+ 'peer': peer,
2504
+ }));
2505
+
2506
+ /// FRAMEWORK-INTERNAL business birth: derive the unborn child's owned key from its deterministic address.
2507
+ Future<NomosScopeKey> nomosOwnedBirthScopeKey(
2508
+ String scope, String namespace, Object? key) async {
2509
+ final out = _asMap(await _call('cryptoOwnedBirthScopeKey', {
2510
+ 'scope': scope,
2511
+ 'namespace': namespace,
2512
+ 'key': key,
2513
+ }));
2514
+ return NomosScopeKey(
2515
+ scope: (out['scope'] ?? scope).toString(),
2516
+ epoch: (out['epoch'] as num?)?.toInt() ?? 0,
2517
+ key: (out['key'] ?? '').toString(),
2518
+ ownedKeySubject: out['ownedKeySubject']?.toString(),
2519
+ ownedCustody: out['ownedCustody']?.toString(),
2520
+ workspace: out['workspace']?.toString(),
2521
+ );
2522
+ }
2523
+
2524
+ /// FRAMEWORK-INTERNAL: issue an opaque request for this session's current device.
2525
+ Future<String> nomosOwnedDeviceRequest(String scope) async =>
2526
+ (_asMap(await _call('cryptoOwnedDeviceRequest', {'scope': scope}))[
2527
+ 'request'] ??
2528
+ '')
2088
2529
  .toString();
2089
2530
 
2531
+ /// FRAMEWORK-INTERNAL: validate an opaque request and prepare the atomic signer + custody offer.
2532
+ Future<Map<String, Object?>> nomosPrepareOwnedDeviceApproval(
2533
+ String request, String scope) async =>
2534
+ _asMap(await _call('cryptoOwnedPrepareApproval', {
2535
+ 'request': request,
2536
+ 'scope': scope,
2537
+ }));
2538
+
2090
2539
  /// Wrap a scope key TO a recipient's public key (committer side): `{hpkeEpk, ct}` base64 ciphertext.
2091
2540
  Future<Map<String, Object?>> cryptoWrapKey(
2092
2541
  {required String recipientPub, required String scopeKey}) async =>
@@ -2098,8 +2547,11 @@ class NomosBridge {
2098
2547
  {required String secret,
2099
2548
  required String hpkeEpk,
2100
2549
  required String ct}) async =>
2101
- (_asMap(await _call('cryptoUnwrapKey',
2102
- {'secret': secret, 'hpkeEpk': hpkeEpk, 'ct': ct}))['scopeKey'] ??
2550
+ (_asMap(await _call('cryptoUnwrapKey', {
2551
+ 'secret': secret,
2552
+ 'hpkeEpk': hpkeEpk,
2553
+ 'ct': ct
2554
+ }))['scopeKey'] ??
2103
2555
  '')
2104
2556
  .toString();
2105
2557
 
@@ -2115,7 +2567,8 @@ class NomosBridge {
2115
2567
  }));
2116
2568
 
2117
2569
  /// A maintained O(1) count / sum.
2118
- Future<int> count(String countId, [String group = '', String? session]) async =>
2570
+ Future<int> count(String countId,
2571
+ [String group = '', String? session]) async =>
2119
2572
  ((_asMap(await _call('count', {
2120
2573
  'countId': countId,
2121
2574
  'group': group,
@@ -2137,13 +2590,31 @@ class NomosBridge {
2137
2590
  /// DERIVED, NOT DRAINED: a resolution write folding in removes its row by derivation.
2138
2591
  /// Empty for a Surfaced-free workspace. (The generated typed client wraps these rows as
2139
2592
  /// `PendingConflictRow` and pairs them with the designated `resolve…` helpers.)
2140
- Future<List<Map<String, dynamic>>> pendingConflicts([String? session]) async =>
2593
+ Future<List<Map<String, dynamic>>> pendingConflicts(
2594
+ [String? session]) async =>
2595
+ _asRows(await _call('pendingConflicts', {
2596
+ if (session != null) 'session': session,
2597
+ }));
2598
+
2599
+ /// Framework-filtered pending conflicts for one generated aggregate type. This is the
2600
+ /// primitive generated clients use; application code never filters the heterogeneous queue.
2601
+ Future<List<Map<String, dynamic>>> pendingConflictsFor(String aggregateType,
2602
+ [String? session]) async =>
2141
2603
  _asRows(await _call('pendingConflicts', {
2604
+ 'aggregateType': aggregateType,
2605
+ if (session != null) 'session': session,
2606
+ }));
2607
+
2608
+ /// Counts-only workspace summary over the maintained conflict index.
2609
+ Future<Map<String, Object?>> pendingConflictSummary(
2610
+ [String? session]) async =>
2611
+ _asMap(await _call('pendingConflictSummary', {
2142
2612
  if (session != null) 'session': session,
2143
2613
  }));
2144
2614
 
2145
2615
  /// Push local writes → the session branch → cloud admission, and pull canonical main back.
2146
- Future<Map<String, Object?>> sync({bool admit = true, String? session}) async =>
2616
+ Future<Map<String, Object?>> sync(
2617
+ {bool admit = true, String? session}) async =>
2147
2618
  _asMap(await _call('sync', {
2148
2619
  'admit': admit,
2149
2620
  if (session != null) 'session': session,
@@ -2198,7 +2669,8 @@ class NomosBridge {
2198
2669
  void _startStatusPoll() {
2199
2670
  _pushStatus(_status); // seed the new listener with the current snapshot
2200
2671
  _pollMetricsOnce();
2201
- _metricsTimer ??= Timer.periodic(const Duration(seconds: 4), (_) => _pollMetricsOnce());
2672
+ _metricsTimer ??=
2673
+ Timer.periodic(const Duration(seconds: 4), (_) => _pollMetricsOnce());
2202
2674
  }
2203
2675
 
2204
2676
  void _stopStatusPoll() {
@@ -2209,19 +2681,28 @@ class NomosBridge {
2209
2681
  Future<void> _pollMetricsOnce() async {
2210
2682
  try {
2211
2683
  final m = await metrics();
2212
- int? n(String k) => m.containsKey(k) ? ((m[k] as num?)?.toInt() ?? 0) : null;
2684
+ int? n(String k) =>
2685
+ m.containsKey(k) ? ((m[k] as num?)?.toInt() ?? 0) : null;
2213
2686
  final ahead = n('ahead') ?? _status.ahead;
2214
2687
  final behind = n('behind') ?? _status.behind;
2215
2688
  // Don't stomp an in-flight transfer's phase; otherwise derive idle/behind/syncing from the counts.
2216
2689
  final phase = _status.transfer != null
2217
2690
  ? _status.phase
2218
- : (behind > 0 ? SyncPhase.behind : (ahead > 0 ? SyncPhase.syncing : SyncPhase.idle));
2691
+ : (behind > 0
2692
+ ? SyncPhase.behind
2693
+ : (ahead > 0 ? SyncPhase.syncing : SyncPhase.idle));
2219
2694
  _pushStatus(_status.copyWith(
2220
- ahead: ahead, behind: behind, saved: n('saved'), onMain: n('onMain'),
2221
- deadLetters: n('deadLetters'), phase: phase, clearError: true,
2695
+ ahead: ahead,
2696
+ behind: behind,
2697
+ saved: n('saved'),
2698
+ onMain: n('onMain'),
2699
+ deadLetters: n('deadLetters'),
2700
+ phase: phase,
2701
+ clearError: true,
2222
2702
  ));
2223
2703
  } catch (e) {
2224
- _pushStatus(_status.copyWith(phase: SyncPhase.error, lastError: e.toString()));
2704
+ _pushStatus(
2705
+ _status.copyWith(phase: SyncPhase.error, lastError: e.toString()));
2225
2706
  }
2226
2707
  }
2227
2708
 
@@ -2237,8 +2718,7 @@ class NomosBridge {
2237
2718
 
2238
2719
  /// LIVE per-layer spans of the local pipeline (begin/end as they happen): mutex wait, githolon author,
2239
2720
  /// push, converge … The host renders in-progress bars so a hang is visible WHILE it blocks, not after.
2240
- Stream<Map<String, Object?>> get spanEvents =>
2241
- _eventPlane._spanEvents.stream;
2721
+ Stream<Map<String, Object?>> get spanEvents => _eventPlane._spanEvents.stream;
2242
2722
 
2243
2723
  /// The buffered recent span events — replay these on first build to recover the BOOT flamepath
2244
2724
  /// (fetch wasm / clone ledger / instantiate) that streamed during connect(), before the UI subscribed.
@@ -2262,8 +2742,21 @@ class NomosBridge {
2262
2742
  final plane = _eventPlane;
2263
2743
  final sid = id ?? 'local-${plane._localSpanSeq++}';
2264
2744
  for (final m in <Map<String, Object?>>[
2265
- {'event': 'span', 'span': {'phase': 'begin', 'id': sid, 'track': track, 'name': name, 'kind': kind, 't0': t0}},
2266
- {'event': 'span', 'span': {'phase': 'end', 'id': sid, 't1': t1}},
2745
+ {
2746
+ 'event': 'span',
2747
+ 'span': {
2748
+ 'phase': 'begin',
2749
+ 'id': sid,
2750
+ 'track': track,
2751
+ 'name': name,
2752
+ 'kind': kind,
2753
+ 't0': t0
2754
+ }
2755
+ },
2756
+ {
2757
+ 'event': 'span',
2758
+ 'span': {'phase': 'end', 'id': sid, 't1': t1}
2759
+ },
2267
2760
  ]) {
2268
2761
  plane._spanBuffer.add(m);
2269
2762
  if (plane._spanBuffer.length > 400) {
@@ -2283,7 +2776,8 @@ class NomosBridge {
2283
2776
  /// them (a HEAD on the serving pack; the host answers Content-Length only, cached ~15s). Sum across your open
2284
2777
  /// workspaces for an aggregate "X MB to sync" total BEFORE downloading. `{ bytes, kind:'delta'|'full'|
2285
2778
  /// 'up-to-date' }`; `bytes` is null if the host omitted the length.
2286
- Future<Map<String, Object?>> syncSize() async => _asMap(await _call('syncSize'));
2779
+ Future<Map<String, Object?>> syncSize() async =>
2780
+ _asMap(await _call('syncSize'));
2287
2781
 
2288
2782
  /// Diagnostics — is the expected law installed/active/drift-free here? (The generated client surfaces it
2289
2783
  /// so the app can answer "can I author this directive, and if not what deploy fixes it".)
@@ -2294,14 +2788,16 @@ class NomosBridge {
2294
2788
  /// subscription tells the runner to drop the watch.
2295
2789
  Stream<List<dynamic>> watch(String queryId,
2296
2790
  [Map<String, Object?> params = const {}, String? session]) =>
2297
- _watchStream(kind: 'query', queryId: queryId, params: params, session: session);
2791
+ _watchStream(
2792
+ kind: 'query', queryId: queryId, params: params, session: session);
2298
2793
  Stream<List<dynamic>> watchById(String id, [String? session]) =>
2299
2794
  _watchStream(kind: 'byId', id: id, session: session);
2300
2795
 
2301
2796
  /// Reactive SPATIAL read: re-emits the R*Tree probe's rows whenever they change locally.
2302
2797
  Stream<List<dynamic>> watchSpatial(String spatialId, NomosBbox bbox,
2303
2798
  [String? session]) =>
2304
- _watchStream(kind: 'spatial', spatialId: spatialId, bbox: bbox, session: session);
2799
+ _watchStream(
2800
+ kind: 'spatial', spatialId: spatialId, bbox: bbox, session: session);
2305
2801
 
2306
2802
  /// Reactive PENDING-CONFLICTS read: re-emits the maintained Surfaced "decisions needed"
2307
2803
  /// rows whenever the index changes (derived, not drained — a resolution write clears its row).
@@ -2331,7 +2827,8 @@ class NomosBridge {
2331
2827
  'compat': compat,
2332
2828
  if (requirement != null) 'requirement': requirement,
2333
2829
  }));
2334
- final v = NomosCompat.fromJson((r['compat'] as Map?)?.cast<String, Object?>());
2830
+ final v =
2831
+ NomosCompat.fromJson((r['compat'] as Map?)?.cast<String, Object?>());
2335
2832
  if (v != null) {
2336
2833
  _eventPlane._lastCompat = v;
2337
2834
  // a session's verdict is per-workspace state on the PLANE (so `session.compat` reads it back).
@@ -2566,14 +3063,23 @@ class NomosSession extends NomosBridge {
2566
3063
  Future<NomosSession> acquireSession(String workspace,
2567
3064
  {Future<NomosSessionCredentials> Function()? ifAbsentCredentials,
2568
3065
  String? authorSecret}) =>
2569
- parent.acquireSession(workspace, ifAbsentCredentials: ifAbsentCredentials, authorSecret: authorSecret);
3066
+ parent.acquireSession(workspace,
3067
+ ifAbsentCredentials: ifAbsentCredentials, authorSecret: authorSecret);
2570
3068
 
2571
3069
  /// A followed ref mounts on the SHARED plane — delegate to the parent (a session never owns mounts).
2572
3070
  @override
2573
3071
  Future<NomosSession> follow(NomosRef ref,
2574
- {String? cloud, String? clientId, String? authToken, bool allowUnborn = false, String? authorSecret}) =>
3072
+ {String? cloud,
3073
+ String? clientId,
3074
+ String? authToken,
3075
+ bool allowUnborn = false,
3076
+ String? authorSecret}) =>
2575
3077
  parent.follow(ref,
2576
- cloud: cloud, clientId: clientId, authToken: authToken, allowUnborn: allowUnborn, authorSecret: authorSecret);
3078
+ cloud: cloud,
3079
+ clientId: clientId,
3080
+ authToken: authToken,
3081
+ allowUnborn: allowUnborn,
3082
+ authorSecret: authorSecret);
2577
3083
 
2578
3084
  @override
2579
3085
  Future<Object?> _call(String op, [Map<String, Object?> args = const {}]) =>
@@ -2599,7 +3105,11 @@ class NomosSession extends NomosBridge {
2599
3105
  String? keyHash,
2600
3106
  String? authToken}) async {
2601
3107
  final v = await canAuthor(directiveId,
2602
- ws: ws, principal: principal, publicKey: publicKey, keyHash: keyHash, authToken: authToken);
3108
+ ws: ws,
3109
+ principal: principal,
3110
+ publicKey: publicKey,
3111
+ keyHash: keyHash,
3112
+ authToken: authToken);
2603
3113
  if (v['ok'] == true) return v;
2604
3114
  final cause = v['cause']?.toString();
2605
3115
  final target = v['ws']?.toString() ?? ws ?? workspace;
@@ -2647,7 +3157,8 @@ class _BridgeForeignReader implements NomosForeignReader {
2647
3157
  // (trustSource + .readsFrom). That gate is the security model (a locator is not a foreign key), not a
2648
3158
  // limitation: unverifiable "evidence" would be the real footgun.
2649
3159
  @override
2650
- Future<ForeignAttestation> Function(String workspace, String id, String type)? get attestForeign => null;
3160
+ Future<ForeignAttestation> Function(String workspace, String id, String type)?
3161
+ get attestForeign => null;
2651
3162
  }
2652
3163
 
2653
3164
  /// A per-workspace read handle on the bridge plane — mounts [_workspace] lazily on the first read.
@@ -2661,8 +3172,7 @@ class _BridgeForeignWorkspaceReader implements NomosForeignWorkspaceReader {
2661
3172
  // Mount ONCE (idempotent on the plane — "first connect wins"). `allowUnborn` so a not-yet-born foreign
2662
3173
  // workspace reads empty (→ ForeignNotFound) rather than throwing. A genuinely unreachable cloud throws,
2663
3174
  // which resolveResult classifies as ForeignUnreachable.
2664
- Future<void> _ensureMounted() =>
2665
- _mounting ??= _bridge.connect(
3175
+ Future<void> _ensureMounted() => _mounting ??= _bridge.connect(
2666
3176
  cloud: _cloud,
2667
3177
  workspace: _workspace,
2668
3178
  session: _workspace,
@@ -2688,10 +3198,14 @@ class _BridgeForeignWorkspaceReader implements NomosForeignWorkspaceReader {
2688
3198
  // a transient read failure is not fatal to the subscription — the next sync tick retries.
2689
3199
  }
2690
3200
  }
3201
+
2691
3202
  ctrl = StreamController<List<Map<String, dynamic>>>(
2692
3203
  onListen: () {
2693
3204
  unawaited(emit());
2694
- sub = _bridge.session(_workspace).syncEvents.listen((_) => unawaited(emit()));
3205
+ sub = _bridge
3206
+ .session(_workspace)
3207
+ .syncEvents
3208
+ .listen((_) => unawaited(emit()));
2695
3209
  },
2696
3210
  onCancel: () async {
2697
3211
  await sub?.cancel();