@githolon/dsl 0.113.3 → 0.114.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 (58) hide show
  1. package/assets/operation-contract.wasm +0 -0
  2. package/compile_package.mjs +23 -3
  3. package/compile_proof.d.mts +7 -0
  4. package/compile_proof.mjs +38 -3
  5. package/dart/CHANGELOG.md +24 -0
  6. package/dart/lib/src/application_framework_calls.dart +9 -5
  7. package/dart/lib/src/generated_call.dart +14 -3
  8. package/dart/lib/src/nms_generated_runtime.dart +7 -0
  9. package/dart/lib/src/node_runner_transport.dart +77 -8
  10. package/dart/lib/src/nomos_trace.dart +107 -0
  11. package/dart/lib/src/reception/dart_reception.dart +148 -11
  12. package/dart/lib/src/rows_watch.dart +390 -0
  13. package/dart/lib/src/runner/node-entry.mjs +2807 -691
  14. package/dart/lib/src/sync_status_projection.dart +94 -1
  15. package/dart/lib/src/sync_status_reducer.dart +5 -2
  16. package/dart/lib/src/test_harness.dart +107 -41
  17. package/dart/lib/src/testing/test_peer.dart +107 -99
  18. package/dart/lib/src/testing/test_peer_host.dart +305 -0
  19. package/dart/lib/src/typed_watch_registry.dart +26 -3
  20. package/dart/pubspec.lock +1 -1
  21. package/dart/pubspec.yaml +2 -2
  22. package/dart/test/background_model_adoption_test.dart +1 -0
  23. package/dart/test/custodian_warming_test.dart +105 -0
  24. package/dart/test/fixtures/offer_watch_order_peer.dart +74 -0
  25. package/dart/test/kernel_author_after_read_test.dart +6 -3
  26. package/dart/test/kernel_fence_test.dart +4 -1
  27. package/dart/test/model_adoption_test.dart +17 -0
  28. package/dart/test/nomos_trace_test.dart +91 -0
  29. package/dart/test/own_offer_watch_order_test.dart +90 -0
  30. package/dart/test/receiving_group_open_test.dart +4 -8
  31. package/dart/test/replica_readiness_test.dart +14 -4
  32. package/dart/test/rows_watch_test.dart +331 -0
  33. package/dart/test/test_peer_host_selection_test.dart +49 -0
  34. package/dart_types/CHANGELOG.md +15 -0
  35. package/dart_types/lib/src/approved_model.dart +22 -5
  36. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +535 -1
  37. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +65 -0
  38. package/dart_types/lib/src/proto/nomos/runtime/v2/application_desk.pb.dart +14 -0
  39. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +359 -2
  40. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pbenum.dart +65 -0
  41. package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pb.dart +20 -2
  42. package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pb.dart +278 -9
  43. package/dart_types/lib/src/proto/nomos/runtime/v2/receiving_group_desk.pb.dart +7 -85
  44. package/dart_types/lib/src/proto/nomos/runtime/v2/residency_desk.pb.dart +35 -0
  45. package/dart_types/pubspec.yaml +1 -1
  46. package/package.json +4 -2
  47. package/src/build_provenance.ts +7 -2
  48. package/src/codegen_dart_client.ts +21 -9
  49. package/src/codegen_dart_facade.ts +1 -0
  50. package/src/codegen_dart_framework_calls.ts +12 -9
  51. package/src/codegen_dart_scenario.ts +7 -0
  52. package/src/codegen_ts.ts +26 -24
  53. package/src/compile_package_main.ts +65 -61
  54. package/src/engine_entry.ts +4 -48
  55. package/src/law_unit_set_identity.ts +44 -0
  56. package/src/query/compile.ts +0 -0
  57. package/src/release_inspection.ts +24 -0
  58. package/src/release_publication.ts +298 -0
package/dart/pubspec.yaml CHANGED
@@ -7,7 +7,7 @@ repository: https://github.com/Captain-App/nomos2
7
7
  description: >-
8
8
  Nomos 2 Dart client — generated domain values, intent payloads and application
9
9
  lifecycle types over the typed Nomos kernel protocol and USDA custody model.
10
- version: 0.67.1
10
+ version: 0.68.0
11
11
 
12
12
  environment:
13
13
  sdk: ^3.5.0
@@ -18,7 +18,7 @@ dependencies:
18
18
  # The pure-data core (wire types, offer codec, NomosReads, schema validation, error
19
19
  # info). nomos_client depends on and re-exports it, so existing imports are unchanged.
20
20
  # For local monorepo dev, pubspec_overrides.yaml points this at ../dart_types.
21
- nomos_types: ^0.12.0
21
+ nomos_types: ^0.13.0
22
22
  fixnum: ^1.1.1
23
23
  json_schema: ^5.2.2
24
24
  crypto: ^3.0.0
@@ -87,6 +87,7 @@ final class _BindingOffice
87
87
  }
88
88
 
89
89
  NomosApprovedModel _model([String digit = '1']) => NomosApprovedModel(
90
+ unitSet: 'a' * 64,
90
91
  lawSha256: digit * 64,
91
92
  reason: 'test release',
92
93
  domainKeys: const ['test'],
@@ -0,0 +1,105 @@
1
+ import 'package:fixnum/fixnum.dart' as fixnum;
2
+ import 'package:nomos_client/src/sync_status_projection.dart';
3
+ import 'package:nomos_client/src/sync_status_reducer.dart';
4
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as protocol;
5
+ import 'package:test/test.dart';
6
+
7
+ void main() {
8
+ final since = DateTime.utc(2026, 10, 4, 9);
9
+ protocol.FollowObservation backoff({protocol.CustodianWarming? warming}) =>
10
+ protocol.FollowObservation(
11
+ workspace: 'estate',
12
+ phase: protocol.FollowObservationPhase.FOLLOW_OBSERVATION_PHASE_BACKOFF,
13
+ detail: 'the custodian is opening this workspace from verified custody',
14
+ retryAtUnixMillis: fixnum.Int64(
15
+ since.add(const Duration(seconds: 3)).millisecondsSinceEpoch),
16
+ attempt: 1,
17
+ custodianWarming: warming,
18
+ );
19
+ protocol.CustodianWarming warming({bool stalled = false}) =>
20
+ protocol.CustodianWarming(
21
+ sinceUnixMillis: fixnum.Int64(since.millisecondsSinceEpoch),
22
+ expectedReadyUnixMillis: fixnum.Int64(
23
+ since.add(const Duration(seconds: 3)).millisecondsSinceEpoch),
24
+ stalledAfterUnixMillis: fixnum.Int64(
25
+ since.add(const Duration(seconds: 15)).millisecondsSinceEpoch),
26
+ stalled: stalled,
27
+ );
28
+
29
+ test('an opening answer is a warming posture with its expected-ready time',
30
+ () {
31
+ final status =
32
+ WorkspaceFollowStatus.fromRuntime(backoff(warming: warming()));
33
+ final posture = status.custodianWarming!;
34
+ expect(posture.since, since.toLocal());
35
+ expect(posture.expectedReadyAt,
36
+ since.add(const Duration(seconds: 3)).toLocal());
37
+ final now = since.add(const Duration(seconds: 1));
38
+ expect(posture.remaining(now), const Duration(seconds: 2));
39
+ expect(posture.elapsed(now), const Duration(seconds: 1));
40
+ expect(status.isWarming(now), isTrue);
41
+ expect(status.isWarmingStalled(now), isFalse);
42
+ });
43
+
44
+ test('an ordinary error retry carries no warming posture', () {
45
+ final status = WorkspaceFollowStatus.fromRuntime(backoff());
46
+ expect(status.phase, WorkspaceFollowPhase.backoff);
47
+ expect(status.custodianWarming, isNull);
48
+ expect(status.isWarming(since), isFalse);
49
+ expect(status.isWarmingStalled(since), isFalse);
50
+ });
51
+
52
+ test('warming past its bound is stalled, by the peer report or by the clock',
53
+ () {
54
+ final reported = WorkspaceFollowStatus.fromRuntime(
55
+ backoff(warming: warming(stalled: true)));
56
+ expect(reported.isWarmingStalled(since), isTrue);
57
+ expect(reported.isWarming(since), isFalse);
58
+
59
+ final quiet =
60
+ WorkspaceFollowStatus.fromRuntime(backoff(warming: warming()));
61
+ final late = since.add(const Duration(seconds: 16));
62
+ expect(quiet.isWarmingStalled(late), isTrue,
63
+ reason: 'a quiet report still escalates once the bound has passed');
64
+ expect(quiet.isWarming(late), isFalse);
65
+ });
66
+
67
+ test('a changed warming posture is a changed follow report', () {
68
+ final projection = NmsSyncStatusProjection();
69
+ final seen = <SyncStatus>[];
70
+ final subscription = projection.applicationStream.listen(seen.add);
71
+ addTearDown(subscription.cancel);
72
+ final plain = WorkspaceFollowStatus.fromRuntime(backoff());
73
+ final warm = WorkspaceFollowStatus.fromRuntime(backoff(warming: warming()));
74
+ projection.updateFollows(1, 0, statuses: [plain]);
75
+ projection.updateFollows(1, 0, statuses: [warm]);
76
+ final status = projection.applicationStatus;
77
+ expect(status.followStatuses.single.custodianWarming, isNotNull);
78
+ final now = since.add(const Duration(seconds: 1));
79
+ expect(status.warmingFollows(now).map((f) => f.workspace), ['estate']);
80
+ expect(status.stalledWarmingFollows(now), isEmpty);
81
+ expect(status.stalledWarmingFollows(since.add(const Duration(minutes: 1))),
82
+ hasLength(1));
83
+ });
84
+
85
+ test('a warming sync round is still owed: neither settled nor an error', () {
86
+ final at = DateTime.utc(2026, 10, 4);
87
+ for (final offering in [true, false]) {
88
+ final status = reduceSyncCompletion(
89
+ const SyncStatus(),
90
+ protocol.SyncCompleted(
91
+ result: protocol.SyncWorkspaceResponse(
92
+ workspace: 'estate',
93
+ currentHead: 'head',
94
+ directOfferDeferred: offering,
95
+ custodianWarmingRetryAfterMillis: 3000,
96
+ ),
97
+ ),
98
+ at,
99
+ );
100
+ expect(status.phase, SyncPhase.syncing);
101
+ expect(status.lastError, isNull);
102
+ expect(status.lastSyncedAt, isNull);
103
+ }
104
+ });
105
+ }
@@ -0,0 +1,74 @@
1
+ // A stand-in Node runner for own_offer_watch_order_test.dart. It speaks the runner's stdio framing
2
+ // (`runtime-v2:` requests in, `runtime-v2-event:` events and `runtime-v2:` replies out) and answers a
3
+ // local offer exactly as the Peer Office does since CAP-755: the change of the watch the offer
4
+ // affected first, then the offer's reply, both in ONE stdout write so they share a chunk.
5
+ import 'dart:convert';
6
+ import 'dart:io';
7
+
8
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as p;
9
+ import 'package:nomos_types/src/proto/google/protobuf/any.pb.dart' as protobuf;
10
+
11
+ const _rows = 'type.nomos.dev/estate.CanvasRowsResult';
12
+
13
+ void main() {
14
+ final subscriptions = <String>[];
15
+ var version = 0;
16
+ String position() => 'f$version';
17
+ stdin
18
+ .transform(utf8.decoder)
19
+ .transform(const LineSplitter())
20
+ .listen((line) {
21
+ if (!line.startsWith('runtime-v2:')) return;
22
+ final request = p.RuntimeRequest.fromBuffer(
23
+ base64Decode(line.substring('runtime-v2:'.length)));
24
+ final response =
25
+ p.RuntimeResponse(protocolVersion: 2, requestId: request.requestId);
26
+ final out = StringBuffer();
27
+ void event(p.WatchChanged changed) => out.writeln('runtime-v2-event:'
28
+ '${base64Encode(p.RuntimeEvent(watchChanged: changed).writeToBuffer())}');
29
+ if (request.hasWatch()) {
30
+ final id = request.watch.subscriptionId;
31
+ subscriptions.add(id);
32
+ event(p.WatchChanged(
33
+ subscriptionId: id,
34
+ applicationResult:
35
+ protobuf.Any(typeUrl: _rows, value: utf8.encode('row=v$version')),
36
+ resultFrontier: position(),
37
+ ));
38
+ response.watchStarted = p.WatchStarted(subscriptionId: id);
39
+ } else if (request.hasUnwatch()) {
40
+ subscriptions.remove(request.unwatch.subscriptionId);
41
+ response.watchStopped =
42
+ p.WatchStopped(subscriptionId: request.unwatch.subscriptionId);
43
+ } else if (request.hasCloseRealm()) {
44
+ response.realmClosed = p.RealmClosed();
45
+ } else if (request.hasKernelCall() &&
46
+ request.kernelCall.request.hasOffer()) {
47
+ final base = position();
48
+ version += 1;
49
+ for (final id in subscriptions) {
50
+ event(p.WatchChanged(
51
+ subscriptionId: id,
52
+ applicationDelta: p.ApplicationResultDelta(
53
+ baseFrontier: base,
54
+ frontier: position(),
55
+ changedRows: protobuf.Any(
56
+ typeUrl: _rows, value: utf8.encode('row=v$version')),
57
+ ),
58
+ ));
59
+ }
60
+ response.kernelCall = p.CallResponse(
61
+ protocolVersion: 1,
62
+ requestId: request.kernelCall.request.requestId,
63
+ offer: p.OfferResponse(
64
+ admitted: p.OfferAdmitted(head: 'head-$version')),
65
+ );
66
+ } else {
67
+ response.failure = p.RuntimeFailure(
68
+ code: 'test.unexpected',
69
+ message: 'unexpected ${request.whichCommand().name}');
70
+ }
71
+ out.writeln('runtime-v2:${base64Encode(response.writeToBuffer())}');
72
+ stdout.write(out.toString());
73
+ });
74
+ }
@@ -23,6 +23,8 @@ import 'dart:math';
23
23
  import 'package:fixnum/fixnum.dart';
24
24
 
25
25
  import 'package:nomos_client/src/testing/test_peer.dart';
26
+ import 'package:nomos_client/src/testing/test_peer_host.dart'
27
+ show NmsTestPeerHostKind;
26
28
  import 'package:nomos_types/nomos_kernel_protocol.dart' as nomos_protocol;
27
29
  import 'package:nomos_types/src/proto/google/protobuf/any.pb.dart'
28
30
  as google_protobuf;
@@ -62,6 +64,7 @@ void main() {
62
64
  fail('NOMOS_CANDIDATE_RUNTIME must point at a staged runtime');
63
65
  }
64
66
  final peer = await NmsTestPeer.start(
67
+ kind: NmsTestPeerHostKind.node,
65
68
  entry: await _testEntry(),
66
69
  runtimeDirectory: runtime,
67
70
  resolveFrom: Directory.current.path,
@@ -69,7 +72,7 @@ void main() {
69
72
  );
70
73
  addTearDown(peer.dispose);
71
74
 
72
- final custodyDir = Directory('${peer.custody.directory.path}/custody');
75
+ final custodyDir = Directory('${peer.custodyDirectory.path}/custody');
73
76
  await custodyDir.create(recursive: true);
74
77
  for (final ws in [_workspace, _home]) {
75
78
  if (!Directory('$custodyRoot/holon-$ws.git').existsSync()) {
@@ -92,7 +95,7 @@ void main() {
92
95
 
93
96
  var sequence = 0;
94
97
  Future<nomos_protocol.CallResponse> describe(String session) =>
95
- peer.transport.callKernel(
98
+ peer.host.kernelCalls.callKernel(
96
99
  session: session,
97
100
  request: nomos_protocol.CallRequest(
98
101
  protocolVersion: 1,
@@ -115,7 +118,7 @@ void main() {
115
118
  String typeUrl) async {
116
119
  final nomos_protocol.CallResponse offered;
117
120
  try {
118
- offered = await peer.transport.callKernel(
121
+ offered = await peer.host.kernelCalls.callKernel(
119
122
  session: session,
120
123
  request: nomos_protocol.CallRequest(
121
124
  protocolVersion: 1,
@@ -20,6 +20,8 @@ import 'dart:io';
20
20
  import 'dart:isolate';
21
21
 
22
22
  import 'package:nomos_client/src/testing/test_peer.dart';
23
+ import 'package:nomos_client/src/testing/test_peer_host.dart'
24
+ show NmsTestPeerHostKind;
23
25
  import 'package:test/test.dart';
24
26
 
25
27
  Future<String> _testEntry() async {
@@ -68,6 +70,7 @@ void main() {
68
70
  .first;
69
71
 
70
72
  final peer = await NmsTestPeer.start(
73
+ kind: NmsTestPeerHostKind.node,
71
74
  entry: await _testEntry(),
72
75
  runtimeDirectory: runtime,
73
76
  resolveFrom: Directory.current.path,
@@ -75,7 +78,7 @@ void main() {
75
78
  );
76
79
  addTearDown(peer.dispose);
77
80
 
78
- final custodyDir = Directory('${peer.custody.directory.path}/custody');
81
+ final custodyDir = Directory('${peer.custodyDirectory.path}/custody');
79
82
  await custodyDir.create(recursive: true);
80
83
  final answers = <String, Object?>{};
81
84
  for (final workspace in workspaces) {
@@ -14,6 +14,9 @@ import 'package:nomos_types/nomos_kernel_protocol.dart' as p;
14
14
  import 'package:test/test.dart';
15
15
 
16
16
  const lawSha = '5efef545fdadae764e29ba678833073e0245b1b76e315d3a5fe71c6a65240a45';
17
+ /// The approved law (CAP-773): the unit set identity of the compiled units.
18
+ const unitSet =
19
+ 'a40efb425bc4de94dacaadfdff69633d300c8fb25a2701e78d592f9fab5e75cb';
17
20
  const otherSha =
18
21
  '48f3b1810000000000000000000000000000000000000000000000000000abcd';
19
22
 
@@ -30,6 +33,7 @@ class _Platform
30
33
 
31
34
  NomosApprovedModel model({List<String> keys = const ['home']}) =>
32
35
  NomosApprovedModel(
36
+ unitSet: unitSet,
33
37
  lawSha256: lawSha,
34
38
  reason: '2026-09-11: retire the value fields; decided by Jack',
35
39
  domainKeys: keys,
@@ -45,6 +49,15 @@ void main() {
45
49
  expect(model().domainKeys, ['home']);
46
50
  expect(
47
51
  () => NomosApprovedModel(
52
+ unitSet: 'not-a-hash',
53
+ lawSha256: lawSha,
54
+ reason: 'r',
55
+ domainKeys: const ['home'],
56
+ loadPackage: () async => Uint8List(0)),
57
+ throwsArgumentError);
58
+ expect(
59
+ () => NomosApprovedModel(
60
+ unitSet: unitSet,
48
61
  lawSha256: 'not-a-hash',
49
62
  reason: 'r',
50
63
  domainKeys: const ['home'],
@@ -52,6 +65,7 @@ void main() {
52
65
  throwsArgumentError);
53
66
  expect(
54
67
  () => NomosApprovedModel(
68
+ unitSet: unitSet,
55
69
  lawSha256: lawSha,
56
70
  reason: ' ',
57
71
  domainKeys: const ['home'],
@@ -59,6 +73,7 @@ void main() {
59
73
  throwsArgumentError);
60
74
  expect(
61
75
  () => NomosApprovedModel(
76
+ unitSet: unitSet,
62
77
  lawSha256: lawSha,
63
78
  reason: 'r',
64
79
  domainKeys: const [],
@@ -66,6 +81,7 @@ void main() {
66
81
  throwsArgumentError);
67
82
  expect(
68
83
  () => NomosApprovedModel(
84
+ unitSet: unitSet,
69
85
  lawSha256: lawSha,
70
86
  reason: 'r',
71
87
  domainKeys: const ['home', ' '],
@@ -109,6 +125,7 @@ void main() {
109
125
  final broken = _Platform()
110
126
  ..registerLawPackageSource(NmsApprovedModelLawPackageSource(
111
127
  NomosApprovedModel(
128
+ unitSet: unitSet,
112
129
  lawSha256: lawSha,
113
130
  reason: 'r',
114
131
  domainKeys: const ['home'],
@@ -0,0 +1,91 @@
1
+ import 'dart:async';
2
+
3
+ import 'package:fixnum/fixnum.dart' as fixnum;
4
+ import 'package:nomos_client/src/nomos_trace.dart';
5
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as nomos_protocol;
6
+ import 'package:nomos_types/src/proto/google/protobuf/any.pb.dart'
7
+ as google_protobuf;
8
+ import 'package:test/test.dart';
9
+
10
+ void main() {
11
+ test('the timeline is off unless NOMOS_TRACE is defined', () {
12
+ expect(NomosTrace.enabled, isFalse);
13
+ });
14
+
15
+ test('requests are described by operation kind, never by argument values',
16
+ () {
17
+ final offer = nomos_protocol.RuntimeRequest(
18
+ protocolVersion: 2,
19
+ requestId: fixnum.Int64(7),
20
+ kernelCall: nomos_protocol.KernelCallRequest(
21
+ session: 'estate-1',
22
+ request: nomos_protocol.CallRequest(
23
+ requestId: 'estate-1:offer:3',
24
+ offer: nomos_protocol.OfferRequest(
25
+ call: nomos_protocol.DirectiveCall(
26
+ domain: 'estate',
27
+ directive: 'placeAssetOnDrawing',
28
+ operation: google_protobuf.Any(
29
+ typeUrl: 'type.nomos.dev/secret',
30
+ value: [1, 2, 3],
31
+ ),
32
+ ),
33
+ ),
34
+ ),
35
+ ),
36
+ );
37
+ expect(NomosTrace.describeRequest(offer),
38
+ 'kernelCall/offer:estate/placeAssetOnDrawing');
39
+ final watch = nomos_protocol.RuntimeRequest(
40
+ watch: nomos_protocol.WatchRequest(
41
+ query: nomos_protocol.QueryRequest(
42
+ rows: nomos_protocol.RowsQuery(id: 'assetsByAssetId'),
43
+ ),
44
+ ),
45
+ );
46
+ expect(
47
+ NomosTrace.describeRequest(watch), 'watch/query:rows:assetsByAssetId');
48
+ expect(
49
+ NomosTrace.describeCall(nomos_protocol.CallRequest(
50
+ query: nomos_protocol.QueryRequest(
51
+ byType: nomos_protocol.ByTypeQuery(aggregateType: 'estate.Asset'),
52
+ ),
53
+ )),
54
+ 'query:byType:estate.Asset',
55
+ );
56
+ });
57
+
58
+ test('a sampled context carries no ids, so span ids stay request-derived',
59
+ () {
60
+ final context = NomosTrace.sampledContext();
61
+ expect(context.sampled, isTrue);
62
+ expect(context.traceId, isEmpty);
63
+ expect(context.spanId, isEmpty);
64
+ });
65
+
66
+ test('listener timing leaves the stream untouched while tracing is off',
67
+ () async {
68
+ final source = Stream<int>.fromIterable([1, 2, 3]);
69
+ expect(identical(NomosTrace.timedListener(source, 'q=x'), source), isTrue);
70
+ });
71
+
72
+ test('listener timing forwards every event and error when on', () async {
73
+ NomosTrace.enabled = true;
74
+ addTearDown(() => NomosTrace.enabled = false);
75
+ final controller = StreamController<int>();
76
+ final seen = <Object>[];
77
+ final done = Completer<void>();
78
+ NomosTrace.timedListener(controller.stream, 'q=x').listen(
79
+ seen.add,
80
+ onError: (Object error) => seen.add('error:$error'),
81
+ onDone: done.complete,
82
+ );
83
+ controller
84
+ ..add(1)
85
+ ..addError('boom')
86
+ ..add(2);
87
+ await controller.close();
88
+ await done.future;
89
+ expect(seen, [1, 'error:boom', 2]);
90
+ });
91
+ }
@@ -0,0 +1,90 @@
1
+ // CAP-755: a local offer's call returns only after its affected watches have delivered.
2
+ //
3
+ // The Peer Office queues the change of every watch a local offer affected before the offer's reply,
4
+ // and the runner writes both to stdout in order, often in one chunk. Their Dart listeners run as
5
+ // microtasks, so without care the awaiting caller could resume first and see its own offer missing
6
+ // from the watch. This drives the real NodeRunnerTransport and DartReception rows watch against a
7
+ // stand-in runner that writes the change and the reply in one stdout write.
8
+ @TestOn('vm')
9
+ library;
10
+
11
+ import 'dart:async';
12
+ import 'dart:convert';
13
+ import 'dart:io';
14
+ import 'dart:isolate';
15
+ import 'dart:typed_data';
16
+
17
+ import 'package:nomos_client/src/nms_generated_runtime.dart';
18
+ import 'package:nomos_client/src/node_runner_transport.dart';
19
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as p;
20
+ import 'package:test/test.dart';
21
+
22
+ final _query = NmsInternalGeneratedQueries.create(
23
+ domain: 'estate',
24
+ query: 'canvasRows',
25
+ protobufType: 'type.nomos.dev/estate.CanvasRowsArgs',
26
+ protobufPayload: Uint8List.fromList([8, 1]),
27
+ );
28
+
29
+ Future<String> _fixture() async {
30
+ final library = await Isolate.resolvePackageUri(
31
+ Uri.parse('package:nomos_client/src/node_runner_transport.dart'));
32
+ // dsl/dart/lib/src/node_runner_transport.dart -> dsl/dart/test/fixtures/...
33
+ return library!
34
+ .resolve('../../test/fixtures/offer_watch_order_peer.dart')
35
+ .toFilePath();
36
+ }
37
+
38
+ void main() {
39
+ test('each local offer\'s watch change reaches its listener before the offer\'s call returns',
40
+ () async {
41
+ final transport = await NodeRunnerTransport.start(
42
+ entry: await _fixture(),
43
+ node: Platform.resolvedExecutable,
44
+ );
45
+ final reception = DartReception(transport);
46
+ addTearDown(() async {
47
+ await reception.dispose();
48
+ await transport.dispose();
49
+ });
50
+
51
+ final timeline = <String>[];
52
+ final first = Completer<void>();
53
+ final watch = reception
54
+ .session('estate')
55
+ .nmsWatchRows<String>(
56
+ _query,
57
+ decode: (result) => [utf8.decode(result.protobufPayload)],
58
+ idOf: (row) => row.split('=').first,
59
+ )
60
+ .listen((rows) {
61
+ timeline.add('watch ${rows.single}');
62
+ if (!first.isCompleted) first.complete();
63
+ });
64
+ addTearDown(watch.cancel);
65
+ await first.future.timeout(const Duration(seconds: 30));
66
+
67
+ // A group of sequential offers, as an application issues them: each awaits the last.
68
+ for (var offer = 1; offer <= 20; offer += 1) {
69
+ final reply = await transport.callKernel(
70
+ session: 'estate',
71
+ request: p.CallRequest(
72
+ protocolVersion: 1,
73
+ requestId: 'offer-$offer',
74
+ offer: p.OfferRequest(),
75
+ ),
76
+ credentialUse: p.CredentialUse.CREDENTIAL_USE_SESSION,
77
+ );
78
+ expect(reply.offer.hasAdmitted(), isTrue);
79
+ timeline.add('returned $offer');
80
+ }
81
+
82
+ expect(timeline, [
83
+ 'watch row=v0',
84
+ for (var offer = 1; offer <= 20; offer += 1) ...[
85
+ 'watch row=v$offer',
86
+ 'returned $offer',
87
+ ],
88
+ ], reason: 'every offer\'s own change is delivered once, before its call returns');
89
+ }, timeout: const Timeout(Duration(minutes: 2)));
90
+ }
@@ -14,7 +14,6 @@ const _principal = 'user:ada';
14
14
  // The office names the receiving group; Reception asks for it. A test scripts
15
15
  // that answer like any other paper.
16
16
  const _group = 'nomos-rg-scripted';
17
- final _lawSha256 = '2' * 64;
18
17
 
19
18
  final class _Office implements PeerOfficeConnection {
20
19
  _Office({this.refuse});
@@ -88,7 +87,7 @@ Future<DartReception> _connected(_Office office) async {
88
87
 
89
88
  void main() {
90
89
  test(
91
- 'the session sends exactly one open paper: contract, principal, session, no seed',
90
+ 'the session sends exactly one open paper: principal, session, no seed',
92
91
  () async {
93
92
  final office = _Office();
94
93
  final reception = await _connected(office);
@@ -97,7 +96,6 @@ void main() {
97
96
  application: 'acme-platform',
98
97
  principal: _principal,
99
98
  authToken: 'id-token',
100
- contract: protocol.ApplicationBirthContract(lawSha256: _lawSha256),
101
99
  verifyHistory: true,
102
100
  );
103
101
 
@@ -109,10 +107,9 @@ void main() {
109
107
  expect(paper.application, 'acme-platform');
110
108
  expect(paper.principal, _principal);
111
109
  expect(utf8.decode(paper.authToken), 'id-token');
112
- expect(paper.contract.lawSha256, _lawSha256);
113
- expect(paper.contract.toProto3Json(), {'lawSha256': _lawSha256},
110
+ expect((paper.toProto3Json()! as Map).keys, isNot(contains('contract')),
114
111
  reason:
115
- 'the contract is law-only: the platform controller is observed by the desk');
112
+ 'no compiled law rides the paper: the Home takes what its platform serves (CAP-773)');
116
113
  expect(paper.genesisSeed, isEmpty, reason: 'seeds are law');
117
114
  expect(paper.verifyHistory, isTrue);
118
115
  expect(papers.single.protocolVersion, 2);
@@ -137,8 +134,7 @@ void main() {
137
134
  application: 'acme-platform',
138
135
  principal: _principal,
139
136
  authToken: 'id-token',
140
- contract: protocol.ApplicationBirthContract(lawSha256: _lawSha256),
141
- ),
137
+ ),
142
138
  throwsA(isA<NomosRuntimeError>().having(
143
139
  (e) => e.info.code,
144
140
  'code',
@@ -128,15 +128,20 @@ void main() {
128
128
 
129
129
  // The guard: every one-shot read reaches the kernel through one of these two
130
130
  // request shapes. Each must sit inside nmsUntilReplicaReady, so a new read
131
- // path cannot hand callers a "not ready yet" as an error.
131
+ // path cannot hand callers a "not ready yet" as an error. A site is where the
132
+ // request is BUILT (its requestId, or the historic request), never a trace line
133
+ // that merely names the same logical id (CAP-741's `call.send`), and "inside"
134
+ // means the readiness wait opens between the site's enclosing member and the
135
+ // site, however much tracing the closure carries.
132
136
  test('every kernel query request is built inside nmsUntilReplicaReady', () {
133
137
  final source = File(
134
138
  'lib/src/reception/dart_reception.dart',
135
139
  ).readAsLinesSync();
140
+ final member = RegExp(r'^ [A-Za-z_]');
136
141
  final requestSites = <int>[];
137
142
  for (var i = 0; i < source.length; i++) {
138
143
  final line = source[i];
139
- if (line.contains(r":query:$") ||
144
+ if ((line.contains('requestId:') && line.contains(r":query:$")) ||
140
145
  line.contains(
141
146
  'historicQuery: nomos_protocol.HistoricQueryRequest(')) {
142
147
  requestSites.add(i);
@@ -144,9 +149,14 @@ void main() {
144
149
  }
145
150
  expect(requestSites, hasLength(2), reason: 'query request sites moved');
146
151
  for (final site in requestSites) {
147
- final window = source.sublist(site - 16 < 0 ? 0 : site - 16, site);
152
+ var start = site;
153
+ while (start > 0 && !member.hasMatch(source[start])) {
154
+ start--;
155
+ }
148
156
  expect(
149
- window.any((line) => line.contains('nmsUntilReplicaReady(')),
157
+ source
158
+ .sublist(start, site)
159
+ .any((line) => line.contains('nmsUntilReplicaReady(')),
150
160
  isTrue,
151
161
  reason: 'dart_reception.dart:${site + 1} sends a kernel query outside '
152
162
  'nmsUntilReplicaReady',