@githolon/dsl 0.101.6 → 0.102.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 (120) hide show
  1. package/dart/CHANGELOG.md +13 -0
  2. package/dart/lib/nomos_client.dart +3 -52
  3. package/dart/lib/src/dev_cloud.dart +5 -0
  4. package/dart/lib/src/home.dart +10 -15
  5. package/dart/lib/src/nms_conformance_runtime.dart +12 -0
  6. package/dart/lib/src/nms_generated_runtime.dart +29 -0
  7. package/dart/lib/src/nms_test_environment.dart +8 -0
  8. package/dart/lib/src/node_runner_transport.dart +56 -18
  9. package/dart/lib/src/provider.dart +5 -1
  10. package/dart/lib/src/record_origin.dart +55 -0
  11. package/dart/lib/src/runner/node-entry.mjs +9105 -0
  12. package/dart/lib/src/runner/offline-install.mjs +4729 -0
  13. package/dart/lib/src/session_bound.dart +23 -10
  14. package/dart/lib/src/source_trust.dart +51 -0
  15. package/dart/lib/src/test_harness.dart +490 -58
  16. package/dart/lib/src/webview_bridge.dart +167 -58
  17. package/dart/lib/src/workspace_name.dart +7 -1
  18. package/dart/pubspec.lock +3 -3
  19. package/dart/pubspec.yaml +4 -2
  20. package/dart/test/acquire_session_credentials_test.dart +34 -17
  21. package/dart/test/acquire_session_test.dart +109 -24
  22. package/dart/test/adv_realm_interference_test.dart +227 -99
  23. package/dart/test/bundle_version_pairing_test.dart +1 -1
  24. package/dart/test/cloud_bound_topology_test.dart +1 -1
  25. package/dart/test/cluster_attested_test.dart +52 -35
  26. package/dart/test/cluster_births_test.dart +5 -2
  27. package/dart/test/cluster_device_keyed_acceptance_test.dart +5 -2
  28. package/dart/test/cluster_local_autosync_test.dart +5 -2
  29. package/dart/test/cluster_scale_test.dart +5 -2
  30. package/dart/test/compat_test.dart +1 -1
  31. package/dart/test/dlq_session_surface_test.dart +2 -1
  32. package/dart/test/errors_test.dart +1 -1
  33. package/dart/test/first_app_gauntlet_test.dart +78 -51
  34. package/dart/test/four_layer_nested_topology_test.dart +1 -1
  35. package/dart/test/harness_phase_timeout_test.dart +1 -1
  36. package/dart/test/home_parity_test.dart +9 -5
  37. package/dart/test/local_sync_inert_test.dart +22 -14
  38. package/dart/test/node_runner_transport_test.dart +9 -5
  39. package/dart/test/null_slot_test.dart +1 -1
  40. package/dart/test/parent_signer_recovery_test.dart +5 -2
  41. package/dart/test/schema_validation_test.dart +1 -1
  42. package/dart/test/sealing_client_test.dart +2 -1
  43. package/dart/test/session_bound_test.dart +92 -119
  44. package/dart/test/stale_bundle_test.dart +1 -1
  45. package/dart/test/test_harness_test.dart +77 -22
  46. package/dart/test/vo_read_typing_test.dart +32 -14
  47. package/dart/test/watch_after_offer_stall_test.dart +2 -1
  48. package/dart/test/webview_bridge_test.dart +17 -4
  49. package/dart/test/wire_test.dart +1 -1
  50. package/dart_types/CHANGELOG.md +8 -0
  51. package/dart_types/lib/nomos_types.dart +7 -6
  52. package/dart_types/lib/src/activity.dart +23 -13
  53. package/dart_types/lib/src/foreign_ref.dart +61 -112
  54. package/dart_types/lib/src/invitation.dart +96 -0
  55. package/dart_types/lib/src/nomos_ref.dart +97 -59
  56. package/dart_types/pubspec.yaml +1 -1
  57. package/dart_types/test/surfaced_test.dart +125 -19
  58. package/package.json +7 -3
  59. package/runtime/generated-application-host.js +8851 -0
  60. package/src/activity.ts +2 -2
  61. package/src/aggregate.ts +24 -1
  62. package/src/application_composition.ts +282 -0
  63. package/src/application_topology.ts +151 -0
  64. package/src/attested_core.ts +2 -2
  65. package/src/authoring_context.ts +9 -0
  66. package/src/birth_meter_ensure.ts +1 -1
  67. package/src/births.ts +53 -5
  68. package/src/build_package.ts +79 -71
  69. package/src/business_birth.ts +93 -10
  70. package/src/clear_directive.ts +3 -1
  71. package/src/codegen_dart.ts +369 -420
  72. package/src/codegen_dart_application.ts +1508 -0
  73. package/src/codegen_dart_client.ts +156 -95
  74. package/src/codegen_dart_scenario.ts +299 -0
  75. package/src/codegen_openapi.ts +8 -3
  76. package/src/codegen_proof.ts +1113 -1137
  77. package/src/codegen_provider_dart.ts +12 -1
  78. package/src/codegen_ts.ts +1176 -496
  79. package/src/compat_package_main.ts +22 -10
  80. package/src/compile_package_main.ts +1536 -337
  81. package/src/contract.ts +10 -4
  82. package/src/dart_barrel_guard.ts +5 -1
  83. package/src/describe_model.ts +13 -5
  84. package/src/directive.ts +244 -5
  85. package/src/engine_entry.ts +56 -13
  86. package/src/engine_javascript.ts +31 -0
  87. package/src/evolve_preview.ts +3 -12
  88. package/src/framework/application_home.ts +24 -10
  89. package/src/framework/capability.ts +15 -1
  90. package/src/framework/domain_lifecycle.ts +8 -0
  91. package/src/framework/framework_aggregates.ts +2 -2
  92. package/src/framework/genesis.ts +11 -1
  93. package/src/framework/home.ts +178 -75
  94. package/src/framework/impure_capability.ts +24 -7
  95. package/src/framework/relations.ts +13 -5
  96. package/src/framework/workspaces.ts +13 -12
  97. package/src/generated_client_names.ts +42 -3
  98. package/src/idof_lint.ts +65 -13
  99. package/src/index.ts +46 -12
  100. package/src/intent.ts +587 -0
  101. package/src/law_check.ts +64 -16
  102. package/src/precondition.ts +16 -4
  103. package/src/read_projection_namespace.ts +2 -7
  104. package/src/recipient.ts +33 -0
  105. package/src/relation_schema.ts +93 -0
  106. package/src/secondary_ensure.ts +11 -2
  107. package/src/shape_compat.ts +11 -1
  108. package/src/usd.ts +30 -8
  109. package/src/usd_layers.ts +447 -246
  110. package/src/usd_state.ts +1 -1
  111. package/src/value_object.ts +49 -13
  112. package/src/wire_encode.ts +124 -26
  113. package/src/workspace_sharing.ts +269 -0
  114. package/src/zod_introspection.ts +84 -0
  115. package/dart/example/nomos_secure_key_store.dart +0 -82
  116. package/dart/example/nomos_webview_transport.dart +0 -63
  117. package/dart/lib/nomos_headless.dart +0 -50
  118. package/dart/lib/testing.dart +0 -14
  119. package/dart/test/co2_product_gauntlet_test.dart +0 -244
  120. package/src/framework/identity.ts +0 -537
@@ -20,22 +20,37 @@ import 'dart:async';
20
20
  import 'dart:convert';
21
21
  import 'dart:io';
22
22
 
23
- import 'package:nomos_client/nomos_headless.dart';
24
- import 'package:nomos_types/nomos_types.dart' show NomosRef;
23
+ import 'package:nomos_client/src/nms_conformance_runtime.dart';
24
+ import 'package:nomos_types/src/nomos_ref.dart' show NmsInternalNomosRefs;
25
25
  import 'package:test/test.dart';
26
26
 
27
27
  String get _root => '${Directory.current.path}/../..';
28
28
  String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
29
29
  String get _installer => '$_root/cloud/web-client/runner/offline-install.ts';
30
- String get _guestbook => '$_root/examples/guestbook/build/guestbook.deploy.json';
31
- String get _surfaced => '$_root/bench/scale/fixtures/surfaced/build/surfaced.deploy.json';
32
- String get _attributed => '$_root/bench/scale/fixtures/attributed/build/attributed.deploy.json';
33
- String get _estateBirth => '$_root/bench/scale/fixtures/estate-birth/build/estate-birth.deploy.json';
34
- String get _runtimeDir => '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
30
+ String get _guestbook =>
31
+ '$_root/examples/guestbook/build/guestbook.deploy.json';
32
+ String get _surfaced =>
33
+ '$_root/bench/scale/fixtures/surfaced/build/surfaced.deploy.json';
34
+ String get _attributed =>
35
+ '$_root/bench/scale/fixtures/attributed/build/attributed.deploy.json';
36
+ String get _estateBirth =>
37
+ '$_root/bench/scale/fixtures/estate-birth/build/estate-birth.deploy.json';
38
+ String get _runtimeDir =>
39
+ Platform.environment['NOMOS_DART_RUNTIME'] ??
40
+ Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
41
+ '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
35
42
 
36
43
  String? _missingPrereq() {
37
- for (final p in [_repoEntry, _installer, _guestbook, _surfaced, _attributed, _estateBirth]) {
38
- if (!File(p).existsSync()) return 'missing $p (compile the fixture / example first)';
44
+ for (final p in [
45
+ _repoEntry,
46
+ _installer,
47
+ _guestbook,
48
+ _surfaced,
49
+ _attributed,
50
+ _estateBirth
51
+ ]) {
52
+ if (!File(p).existsSync())
53
+ return 'missing $p (compile the fixture / example first)';
39
54
  }
40
55
  if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
41
56
  !File('$_runtimeDir/packages.json').existsSync()) {
@@ -50,7 +65,12 @@ Future<String> _sign(NomosSession s, String author, String message) =>
50
65
  s.offerDirective(
51
66
  domain: 'guestbook',
52
67
  directiveId: 'signGuestbook',
53
- payload: {'author': author, 'message': message, 'mood': 'delighted', 'signedAt': _nowIso()},
68
+ payload: {
69
+ 'author': author,
70
+ 'message': message,
71
+ 'mood': 'delighted',
72
+ 'signedAt': _nowIso()
73
+ },
54
74
  actor: author);
55
75
 
56
76
  void main() {
@@ -82,14 +102,16 @@ void main() {
82
102
  await harness.dispose();
83
103
  });
84
104
 
85
- test('1. concurrent offers to different sessions: no cross-chain bleed, independent heads',
105
+ test(
106
+ '1. concurrent offers to different sessions: no cross-chain bleed, independent heads',
86
107
  () async {
87
108
  if (skip != null) return markTestSkipped(skip);
88
109
  const gbs = ['gb-1', 'gb-2', 'gb-3'];
89
110
  final sw = Stopwatch()..start();
90
111
  final futs = <Future<Object?>>[
91
112
  for (final ws in gbs)
92
- for (var i = 0; i < 4; i++) _sign(harness.session(ws), 'author-$ws', 'm$i'),
113
+ for (var i = 0; i < 4; i++)
114
+ _sign(harness.session(ws), 'author-$ws', 'm$i'),
93
115
  for (var i = 0; i < 4; i++)
94
116
  harness.session('surf').offerDirective(
95
117
  domain: 'docs',
@@ -103,7 +125,8 @@ void main() {
103
125
  payload: {'owner': 'owner-attr', 'name': 'site-$i'},
104
126
  actor: 'owner-attr'),
105
127
  ];
106
- final heads = await Future.wait(futs); // 20 concurrent offers across 5 sessions
128
+ final heads =
129
+ await Future.wait(futs); // 20 concurrent offers across 5 sessions
107
130
  sw.stop();
108
131
  expect(heads, everyElement(isNotNull));
109
132
  // ack latency stays sane under concurrency (generous CI margin)
@@ -112,17 +135,28 @@ void main() {
112
135
 
113
136
  // each chain holds ONLY its own writes
114
137
  for (final ws in gbs) {
115
- final mine = await harness.session(ws).query('entriesByAuthor', {'author': 'author-$ws'});
138
+ final mine = await harness
139
+ .session(ws)
140
+ .query('entriesByAuthor', {'author': 'author-$ws'});
116
141
  expect(mine, hasLength(4), reason: '$ws holds its own 4 writes');
117
142
  for (final other in gbs.where((o) => o != ws)) {
118
- final theirs =
119
- await harness.session(ws).query('entriesByAuthor', {'author': 'author-$other'});
120
- expect(theirs, isEmpty, reason: "$ws must not hold $other's writes (cross-session bleed)");
143
+ final theirs = await harness
144
+ .session(ws)
145
+ .query('entriesByAuthor', {'author': 'author-$other'});
146
+ expect(theirs, isEmpty,
147
+ reason:
148
+ "$ws must not hold $other's writes (cross-session bleed)");
121
149
  }
122
150
  }
123
- expect(await harness.session('surf').query('docsByOwner', {'owner': 'owner-surf'}),
151
+ expect(
152
+ await harness
153
+ .session('surf')
154
+ .query('docsByOwner', {'owner': 'owner-surf'}),
124
155
  hasLength(4));
125
- expect(await harness.session('attr').query('sitesByOwner', {'owner': 'owner-attr'}),
156
+ expect(
157
+ await harness
158
+ .session('attr')
159
+ .query('sitesByOwner', {'owner': 'owner-attr'}),
126
160
  hasLength(4));
127
161
 
128
162
  // heads are independent per session
@@ -130,10 +164,12 @@ void main() {
130
164
  for (final ws in [...gbs, 'surf', 'attr']) {
131
165
  tips.add(await harness.session(ws).head());
132
166
  }
133
- expect(tips, hasLength(5), reason: 'every session has its own ledger tip');
167
+ expect(tips, hasLength(5),
168
+ reason: 'every session has its own ledger tip');
134
169
  });
135
170
 
136
- test('2. a watch on session A fires while session B floods (no starvation, no bleed, ordered)',
171
+ test(
172
+ '2. a watch on session A fires while session B floods (no starvation, no bleed, ordered)',
137
173
  () async {
138
174
  if (skip != null) return markTestSkipped(skip);
139
175
  final a = harness.session('gb-1');
@@ -144,9 +180,11 @@ void main() {
144
180
  final emissionsIdle = <int>[];
145
181
  final subA = a.watch('entriesByAuthor', {'author': 'watched'}).listen(
146
182
  (rows) => emissionsA.add(rows.length));
147
- final subIdle = idle.watch('sitesByOwner', {'owner': 'nobody-here'}).listen(
148
- (rows) => emissionsIdle.add(rows.length));
149
- await Future<void>.delayed(const Duration(milliseconds: 600)); // initial emissions
183
+ final subIdle = idle
184
+ .watch('sitesByOwner', {'owner': 'nobody-here'}).listen(
185
+ (rows) => emissionsIdle.add(rows.length));
186
+ await Future<void>.delayed(
187
+ const Duration(milliseconds: 600)); // initial emissions
150
188
  final idleBase = emissionsIdle.length;
151
189
 
152
190
  // B floods 25 writes while A takes 3 interleaved writes
@@ -172,16 +210,19 @@ void main() {
172
210
  await subA.cancel();
173
211
  await subIdle.cancel();
174
212
  expect(emissionsA.isNotEmpty && emissionsA.last == 3, isTrue,
175
- reason: "A's watch starved or missed rows under B's flood: $emissionsA");
213
+ reason:
214
+ "A's watch starved or missed rows under B's flood: $emissionsA");
176
215
  for (var i = 1; i < emissionsA.length; i++) {
177
216
  expect(emissionsA[i], greaterThanOrEqualTo(emissionsA[i - 1]),
178
217
  reason: 'per-subscription ordering must be monotonic: $emissionsA');
179
218
  }
180
219
  expect(emissionsIdle.length, idleBase,
181
- reason: "the idle sibling watch got spurious emissions from B's flood: $emissionsIdle");
220
+ reason:
221
+ "the idle sibling watch got spurious emissions from B's flood: $emissionsIdle");
182
222
  });
183
223
 
184
- test('3a. follow(ref) + racing acquireSession during a sibling offer flood', () async {
224
+ test('3a. follow(ref) + racing acquireSession during a sibling offer flood',
225
+ () async {
185
226
  if (skip != null) return markTestSkipped(skip);
186
227
  final flood = () async {
187
228
  for (var i = 0; i < 10; i++) {
@@ -189,10 +230,11 @@ void main() {
189
230
  }
190
231
  }();
191
232
  // follow an already-open sibling mid-flood → same handle, functional
192
- final followed = await plane
193
- .follow(NomosRef(workspace: 'gb-1', type: 'nomos.Workspace', id: 'gb-1'));
233
+ final followed = await plane.follow(NmsInternalNomosRefs.create<Object?>(
234
+ workspace: 'gb-1', type: 'nomos.Workspace', id: 'gb-1'));
194
235
  expect(followed.workspace, 'gb-1');
195
- expect(await followed.query('entriesByAuthor', {'author': 'nobody'}), isEmpty);
236
+ expect(await followed.query('entriesByAuthor', {'author': 'nobody'}),
237
+ isEmpty);
196
238
 
197
239
  // concurrent acquireSession single-flights to the SAME handle
198
240
  final pair = await Future.wait([
@@ -200,11 +242,13 @@ void main() {
200
242
  plane.acquireSession('gb-2'),
201
243
  ]);
202
244
  expect(identical(pair[0], pair[1]), isTrue,
203
- reason: 'two concurrent acquireSession calls must yield the same session');
245
+ reason:
246
+ 'two concurrent acquireSession calls must yield the same session');
204
247
  await flood;
205
248
  });
206
249
 
207
- test('3b. FINDER: a FAILED connect must not leave a phantom session acquireSession trusts',
250
+ test(
251
+ '3b. FINDER: a FAILED connect must not leave a phantom session acquireSession trusts',
208
252
  () async {
209
253
  if (skip != null) return markTestSkipped(skip);
210
254
  // a garbage restoreFrom makes the runner's connect throw AFTER the Dart bridge has
@@ -212,7 +256,9 @@ void main() {
212
256
  Object? err;
213
257
  try {
214
258
  await plane.connect(
215
- workspace: 'phantom-ws', session: 'phantom-ws', restoreFrom: '%%%not-base64%%%');
259
+ workspace: 'phantom-ws',
260
+ session: 'phantom-ws',
261
+ restoreFrom: '%%%not-base64%%%');
216
262
  } catch (e) {
217
263
  err = e;
218
264
  }
@@ -224,7 +270,8 @@ void main() {
224
270
  var credentialsAsked = false;
225
271
  Object? acquireOutcome;
226
272
  try {
227
- final s = await plane.acquireSession('phantom-ws', ifAbsentCredentials: () async {
273
+ final s = await plane.acquireSession('phantom-ws',
274
+ ifAbsentCredentials: () async {
228
275
  credentialsAsked = true;
229
276
  throw StateError('credentials path taken (correct) — stop here');
230
277
  });
@@ -238,12 +285,14 @@ void main() {
238
285
  acquireOutcome is! StateError &&
239
286
  acquireOutcome != 'live session';
240
287
  expect(phantom, isFalse,
241
- reason: 'PHANTOM SESSION: after a failed connect, acquireSession("phantom-ws") '
288
+ reason:
289
+ 'PHANTOM SESSION: after a failed connect, acquireSession("phantom-ws") '
242
290
  'returned a dead handle (ops refuse) without asking for credentials — '
243
291
  'observed: $acquireOutcome');
244
292
  });
245
293
 
246
- test('4. pendingConflicts/activityLog on A are unaffected by B (same-named queries isolated)',
294
+ test(
295
+ '4. pendingConflicts/activityLog on A are unaffected by B (same-named queries isolated)',
247
296
  () async {
248
297
  if (skip != null) return markTestSkipped(skip);
249
298
  final surf = harness.session('surf');
@@ -255,26 +304,35 @@ void main() {
255
304
  }
256
305
  final after = await surf.pendingConflicts();
257
306
  expect(after, equals(before),
258
- reason: "surf's pendingConflicts moved on a sibling's writes (cache/key bleed)");
307
+ reason:
308
+ "surf's pendingConflicts moved on a sibling's writes (cache/key bleed)");
259
309
 
260
310
  // activityLog rows belong to the session's OWN chain only
261
311
  final actGb2 = await harness.session('gb-2').activityLog(limit: 100);
262
312
  final foreign = actGb2.where((r) =>
263
313
  r['directiveId'] == 'createSite' ||
264
314
  r['directiveId'] == 'createDoc' ||
265
- (r['payload'] is Map && (r['payload'] as Map)['author'] == 'iso-flood'));
315
+ (r['payload'] is Map &&
316
+ (r['payload'] as Map)['author'] == 'iso-flood'));
266
317
  expect(foreign, isEmpty,
267
- reason: "gb-2's activityLog carries sibling-session intents: ${foreign.toList()}");
318
+ reason:
319
+ "gb-2's activityLog carries sibling-session intents: ${foreign.toList()}");
268
320
 
269
321
  // the SAME query id with the SAME params on two sessions answers per-session
270
- final q1 = await harness.session('gb-1').query('entriesByAuthor', {'author': 'iso-flood'});
271
- final q2 = await harness.session('gb-2').query('entriesByAuthor', {'author': 'iso-flood'});
322
+ final q1 = await harness
323
+ .session('gb-1')
324
+ .query('entriesByAuthor', {'author': 'iso-flood'});
325
+ final q2 = await harness
326
+ .session('gb-2')
327
+ .query('entriesByAuthor', {'author': 'iso-flood'});
272
328
  expect(q1, hasLength(10));
273
329
  expect(q2, isEmpty,
274
- reason: 'same-named query with same params leaked rows across sessions (cache keying)');
330
+ reason:
331
+ 'same-named query with same params leaked rows across sessions (cache keying)');
275
332
  });
276
333
 
277
- test('5. re-connect (atomic replace) during in-flight offers: everything settles typed',
334
+ test(
335
+ '5. re-connect (atomic replace) during in-flight offers: everything settles typed',
278
336
  () async {
279
337
  if (skip != null) return markTestSkipped(skip);
280
338
  final s = harness.session('gb-3');
@@ -282,53 +340,69 @@ void main() {
282
340
  // → identity change → dispose-then-connect under the hood)
283
341
  final futs = [
284
342
  for (var i = 0; i < 10; i++)
285
- _sign(s, 'replace-race', 'r$i').then<Object?>((h) => 'ok:$h', onError: (Object e) => e)
343
+ _sign(s, 'replace-race', 'r$i')
344
+ .then<Object?>((h) => 'ok:$h', onError: (Object e) => e)
286
345
  ];
287
- await plane.connect(workspace: 'gb-3', session: 'gb-3', clientId: 'replacer-1');
346
+ await plane.connect(
347
+ workspace: 'gb-3', session: 'gb-3', clientId: 'replacer-1');
288
348
 
289
- final settled = await Future.wait(futs).timeout(const Duration(seconds: 60),
290
- onTimeout: () =>
291
- throw StateError('offers in flight across an atomic replace HUNG (never settled)'));
349
+ final settled = await Future.wait(futs).timeout(
350
+ const Duration(seconds: 60),
351
+ onTimeout: () => throw StateError(
352
+ 'offers in flight across an atomic replace HUNG (never settled)'));
292
353
  // every outcome is either an admitted head or a TYPED error — never a hang, never null
293
354
  for (final r in settled) {
294
355
  expect(r, isNotNull);
295
356
  if (r is! String) {
296
- expect(r, isA<Exception>(), reason: 'a race loser must fail typed, got: $r');
357
+ expect(r, isA<Exception>(),
358
+ reason: 'a race loser must fail typed, got: $r');
297
359
  }
298
360
  }
299
361
 
300
362
  // the replaced session is consistent: chain verifies, reads serve, writes admit
301
363
  final v = await harness.session('gb-3').verifyChain();
302
364
  expect(v['valid'], isTrue,
303
- reason: 'in-flight offers interleaved with the atomic replace corrupted the chain: $v');
365
+ reason:
366
+ 'in-flight offers interleaved with the atomic replace corrupted the chain: $v');
304
367
  final admitted = settled.whereType<String>().length;
305
- final rows =
306
- await harness.session('gb-3').query('entriesByAuthor', {'author': 'replace-race'});
368
+ final rows = await harness
369
+ .session('gb-3')
370
+ .query('entriesByAuthor', {'author': 'replace-race'});
307
371
  expect(rows.length, admitted,
308
- reason: '$admitted offers acked ok but ${rows.length} rows visible after the replace '
372
+ reason:
373
+ '$admitted offers acked ok but ${rows.length} rows visible after the replace '
309
374
  '(an acked write was lost or duplicated across the replace window)');
310
375
  await _sign(harness.session('gb-3'), 'post-replace', 'alive');
311
- expect(await harness.session('gb-3').query('entriesByAuthor', {'author': 'post-replace'}),
376
+ expect(
377
+ await harness
378
+ .session('gb-3')
379
+ .query('entriesByAuthor', {'author': 'post-replace'}),
312
380
  hasLength(1));
313
381
  // siblings untouched by gb-3's replace
314
- expect(await harness.session('gb-1').query('entriesByAuthor', {'author': 'nobody'}), isEmpty);
382
+ expect(
383
+ await harness
384
+ .session('gb-1')
385
+ .query('entriesByAuthor', {'author': 'nobody'}),
386
+ isEmpty);
315
387
  });
316
388
 
317
- test('6. born child resident-then-session: one consistent view, replace-safe', () async {
389
+ test(
390
+ '6. born child resident-then-session: one consistent view, replace-safe',
391
+ () async {
318
392
  if (skip != null) return markTestSkipped(skip);
319
393
  final fw = harness.frameworkHash;
320
394
  final law = harness.lawHashes('home-birth').first;
321
395
  final outcome = await harness.session('home-birth').offerCreates(
322
- domain: 'home',
323
- directiveId: 'birthEstateWorkspace',
324
- payload: {
325
- 'owner': 'adv-owner',
326
- 'frameworkHash': fw,
327
- 'lawHash': law,
328
- 'bornAt': _nowIso(),
329
- },
330
- actor: 'adv-owner',
331
- );
396
+ domain: 'home',
397
+ directiveId: 'birthEstateWorkspace',
398
+ payload: {
399
+ 'owner': 'adv-owner',
400
+ 'frameworkHash': fw,
401
+ 'lawHash': law,
402
+ 'bornAt': _nowIso(),
403
+ },
404
+ actor: 'adv-owner',
405
+ );
332
406
  expect(outcome.born, hasLength(1));
333
407
  expect(outcome.birthOutcomes.first['status'], 'born');
334
408
  final child = outcome.born.single;
@@ -336,59 +410,85 @@ void main() {
336
410
  // the child is resident BEFORE any session opens it — listWorkspaces sees it
337
411
  final listedPre = await plane.listWorkspaces();
338
412
  expect(listedPre.map((w) => w['workspace']), contains(child),
339
- reason: 'a born child is resident on the plane before it is connected');
413
+ reason:
414
+ 'a born child is resident on the plane before it is connected');
340
415
 
341
416
  // open it as a session (mount in place) and read the genesis seed
342
417
  await plane.connect(workspace: child, session: child);
343
- final rows1 = await plane.session(child).query('estatesByOwner', {'owner': 'adv-owner'});
418
+ final rows1 = await plane
419
+ .session(child)
420
+ .query('estatesByOwner', {'owner': 'adv-owner'});
344
421
  expect(rows1, hasLength(1));
345
422
 
346
423
  // write into the child, then RE-open it with a new clientId (identity replace on a
347
424
  // born-resident workspace) — the write must survive (one holon's truth, not two)
348
- await plane.session(child).offerDirective(
349
- domain: 'estate',
350
- directiveId: 'renameEstate',
351
- payload: {'estateName': child, 'newName': 'renamed'},
352
- actor: 'adv-owner').catchError((Object _) => ''); // directive may not exist — best-effort
353
- await _noThrow(() => plane.connect(workspace: child, session: child, clientId: 'child-re'));
354
- final rows2 = await plane.session(child).query('estatesByOwner', {'owner': 'adv-owner'});
425
+ await plane
426
+ .session(child)
427
+ .offerDirective(
428
+ domain: 'estate',
429
+ directiveId: 'renameEstate',
430
+ payload: {'estateName': child, 'newName': 'renamed'},
431
+ actor: 'adv-owner')
432
+ .catchError(
433
+ (Object _) => ''); // directive may not exist — best-effort
434
+ await _noThrow(() => plane.connect(
435
+ workspace: child, session: child, clientId: 'child-re'));
436
+ final rows2 = await plane
437
+ .session(child)
438
+ .query('estatesByOwner', {'owner': 'adv-owner'});
355
439
  expect(rows2, hasLength(1),
356
- reason: 're-opening a born-resident child must present the SAME holon truth');
440
+ reason:
441
+ 're-opening a born-resident child must present the SAME holon truth');
357
442
 
358
443
  // the parent is unaffected
359
444
  expect(
360
- await plane.session('home-birth').verifyChain().then((v) => v['valid']), isTrue);
445
+ await plane
446
+ .session('home-birth')
447
+ .verifyChain()
448
+ .then((v) => v['valid']),
449
+ isTrue);
361
450
  });
362
451
 
363
- test('7. error isolation: a refusal on one session never poisons siblings', () async {
452
+ test('7. error isolation: a refusal on one session never poisons siblings',
453
+ () async {
364
454
  if (skip != null) return markTestSkipped(skip);
365
455
  // unknown directive → typed refusal
366
456
  await expectLater(
367
457
  harness.session('gb-1').offerDirective(
368
- domain: 'guestbook', directiveId: 'noSuchDirective', payload: {'x': 1}),
458
+ domain: 'guestbook',
459
+ directiveId: 'noSuchDirective',
460
+ payload: {'x': 1}),
369
461
  throwsA(anything));
370
462
  // schema-violating payload → typed refusal
371
463
  await expectLater(
372
464
  harness.session('attr').offerDirective(
373
- domain: 'sites', directiveId: 'createSite', payload: {'owner': '', 'name': ''}),
465
+ domain: 'sites',
466
+ directiveId: 'createSite',
467
+ payload: {'owner': '', 'name': ''}),
374
468
  throwsA(anything));
375
469
  // unknown query → typed refusal, not a hang
376
470
  await expectLater(
377
- harness.session('surf').query('noSuchQuery', {'k': 'v'}), throwsA(anything));
471
+ harness.session('surf').query('noSuchQuery', {'k': 'v'}),
472
+ throwsA(anything));
378
473
 
379
474
  // every sibling still serves reads AND admits writes
380
475
  for (final ws in ['gb-1', 'gb-2', 'gb-3', 'surf', 'attr', 'home-birth']) {
381
476
  final h = await harness.session(ws).head();
382
- expect(h, isNotEmpty, reason: '$ws must still answer after a sibling refusal');
477
+ expect(h, isNotEmpty,
478
+ reason: '$ws must still answer after a sibling refusal');
383
479
  }
384
480
  await _sign(harness.session('gb-2'), 'healthy', 'still-alive');
385
- expect(await harness.session('gb-2').query('entriesByAuthor', {'author': 'healthy'}),
481
+ expect(
482
+ await harness
483
+ .session('gb-2')
484
+ .query('entriesByAuthor', {'author': 'healthy'}),
386
485
  hasLength(1));
387
486
  });
388
487
  });
389
488
 
390
489
  group('runner-level LRU (maxMounted: 2)', () {
391
- test('8. FINDER: watches + session state across LRU park/remount', () async {
490
+ test('8. FINDER: watches + session state across LRU park/remount',
491
+ () async {
392
492
  if (skip != null) return markTestSkipped(skip);
393
493
  final workdir = await Directory.systemTemp.createTemp('adv-lru-');
394
494
  NodeRunnerTransport? transport;
@@ -398,21 +498,34 @@ void main() {
398
498
  for (final ws in ['lru-1', 'lru-2', 'lru-3']) {
399
499
  final out = '${workdir.path}/$ws.bin';
400
500
  final r = await Process.run('node', [
401
- _installer, '--deploy', _guestbook, '--workspace', ws, '--out', out,
402
- '--runtime', _runtimeDir,
501
+ _installer,
502
+ '--deploy',
503
+ _guestbook,
504
+ '--workspace',
505
+ ws,
506
+ '--out',
507
+ out,
508
+ '--runtime',
509
+ _runtimeDir,
403
510
  ]);
404
- expect(r.exitCode, 0, reason: 'offline install of $ws failed: ${r.stderr}');
511
+ expect(r.exitCode, 0,
512
+ reason: 'offline install of $ws failed: ${r.stderr}');
405
513
  snaps[ws] = base64Encode(await File(out).readAsBytes());
406
514
  }
407
515
  transport = await NodeRunnerTransport.start(
408
- entry: _repoEntry, node: 'node', environment: {'NOMOS_OFFLINE': '1'});
516
+ entry: _repoEntry,
517
+ node: 'node',
518
+ environment: {'NOMOS_OFFLINE': '1'});
409
519
  final bridge = NomosBridge(transport);
410
520
  await bridge.ready.timeout(const Duration(seconds: 30));
411
521
 
412
522
  // FIRST connect fixes the realm budget: maxMounted 2
413
523
  await bridge.connect(
414
- workspace: 'lru-1', session: 'lru-1', restoreFrom: snaps['lru-1'],
415
- allowUnborn: true, maxMounted: 2);
524
+ workspace: 'lru-1',
525
+ session: 'lru-1',
526
+ restoreFrom: snaps['lru-1'],
527
+ allowUnborn: true,
528
+ maxMounted: 2);
416
529
  final s1 = bridge.session('lru-1');
417
530
  await _sign(s1, 'survivor', 'before-eviction'); // un-synced local write
418
531
 
@@ -423,21 +536,30 @@ void main() {
423
536
  onError: (Object e) => watchError = e);
424
537
  await Future<void>.delayed(const Duration(milliseconds: 800));
425
538
  expect(emissions.isNotEmpty && emissions.last == 1, isTrue,
426
- reason: 'precondition: the watch saw the pre-eviction row: $emissions');
539
+ reason:
540
+ 'precondition: the watch saw the pre-eviction row: $emissions');
427
541
 
428
542
  // mount two more workspaces → lru-1 is LRU-evicted (parked)
429
543
  await bridge.connect(
430
- workspace: 'lru-2', session: 'lru-2', restoreFrom: snaps['lru-2'], allowUnborn: true);
544
+ workspace: 'lru-2',
545
+ session: 'lru-2',
546
+ restoreFrom: snaps['lru-2'],
547
+ allowUnborn: true);
431
548
  await bridge.connect(
432
- workspace: 'lru-3', session: 'lru-3', restoreFrom: snaps['lru-3'], allowUnborn: true);
549
+ workspace: 'lru-3',
550
+ session: 'lru-3',
551
+ restoreFrom: snaps['lru-3'],
552
+ allowUnborn: true);
433
553
  final listed = await bridge.listWorkspaces();
434
554
  final l1 = listed.firstWhere((w) => w['workspace'] == 'lru-1');
435
- expect(l1['parked'], isTrue, reason: 'lru-1 should be LRU-parked: $listed');
555
+ expect(l1['parked'], isTrue,
556
+ reason: 'lru-1 should be LRU-parked: $listed');
436
557
 
437
558
  // (a) session ops transparently remount from the parked snapshot, state intact
438
559
  final rows = await s1.query('entriesByAuthor', {'author': 'survivor'});
439
560
  expect(rows, hasLength(1),
440
- reason: 'the un-synced local write must survive the park/remount cycle');
561
+ reason:
562
+ 'the un-synced local write must survive the park/remount cycle');
441
563
 
442
564
  // (b) the watch must SURVIVE the eviction+remount (or at least error TYPED —
443
565
  // a silently-dead stream is the bug)
@@ -449,14 +571,20 @@ void main() {
449
571
  await Future<void>.delayed(const Duration(milliseconds: 200));
450
572
  }
451
573
  await sub.cancel();
452
- final died = (emissions.isEmpty || emissions.last < 2) && watchError == null;
574
+ final died =
575
+ (emissions.isEmpty || emissions.last < 2) && watchError == null;
453
576
  expect(died, isFalse,
454
- reason: 'SILENT WATCH DEATH: the lru-1 watch stopped emitting after an LRU '
577
+ reason:
578
+ 'SILENT WATCH DEATH: the lru-1 watch stopped emitting after an LRU '
455
579
  'eviction+remount, with NO error event — emissions=$emissions '
456
580
  '(a Flutter app bound to this stream shows stale data forever)');
457
581
  } finally {
458
- try { await transport?.dispose(); } catch (_) {}
459
- try { await workdir.delete(recursive: true); } catch (_) {}
582
+ try {
583
+ await transport?.dispose();
584
+ } catch (_) {}
585
+ try {
586
+ await workdir.delete(recursive: true);
587
+ } catch (_) {}
460
588
  }
461
589
  });
462
590
  });
@@ -4,7 +4,7 @@
4
4
  import 'dart:convert';
5
5
  import 'dart:io';
6
6
 
7
- import 'package:nomos_client/nomos_headless.dart';
7
+ import 'package:nomos_client/src/nms_conformance_runtime.dart';
8
8
  import 'package:test/test.dart';
9
9
 
10
10
  void main() {
@@ -18,7 +18,7 @@ import 'dart:convert';
18
18
  import 'dart:io';
19
19
 
20
20
  import 'package:crypto/crypto.dart' show sha256;
21
- import 'package:nomos_client/nomos_headless.dart';
21
+ import 'package:nomos_client/src/nms_conformance_runtime.dart';
22
22
  import 'package:test/test.dart';
23
23
 
24
24
  String get _root => '${Directory.current.path}/../..';