@githolon/dsl 0.117.0 → 0.119.9

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 (36) hide show
  1. package/assets/operation-contract.wasm +0 -0
  2. package/compile_proof.mjs +16 -0
  3. package/dart/CHANGELOG.md +45 -0
  4. package/dart/lib/src/application_status.dart +58 -2
  5. package/dart/lib/src/peer_application_streams.dart +7 -1
  6. package/dart/lib/src/reception/dart_reception.dart +92 -12
  7. package/dart/lib/src/reception/reception_session.dart +10 -2
  8. package/dart/lib/src/replica_readiness.dart +200 -50
  9. package/dart/lib/src/runner/node-entry.mjs +2480 -1021
  10. package/dart/lib/src/sync_status_projection.dart +346 -4
  11. package/dart/lib/src/sync_status_reducer.dart +44 -0
  12. package/dart/lib/src/test_harness.dart +1 -0
  13. package/dart/lib/src/typed_watch_registry.dart +67 -7
  14. package/dart/pubspec.lock +1 -1
  15. package/dart/pubspec.yaml +2 -2
  16. package/dart/test/copy_repair_status_test.dart +238 -0
  17. package/dart/test/reads_never_wait_test.dart +387 -0
  18. package/dart/test/replica_readiness_test.dart +223 -109
  19. package/dart/test/sync_status_reducer_test.dart +39 -0
  20. package/dart/test/typed_watch_registry_test.dart +69 -0
  21. package/dart/test/unshared_work_status_test.dart +269 -0
  22. package/dart_types/CHANGELOG.md +18 -0
  23. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +320 -85
  24. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +57 -1
  25. package/dart_types/lib/src/proto/nomos/runtime/v2/maintenance_desk.pb.dart +14 -0
  26. package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pbenum.dart +25 -1
  27. package/dart_types/lib/src/runtime_outcome.dart +17 -0
  28. package/dart_types/pubspec.yaml +1 -1
  29. package/package.json +6 -2
  30. package/src/codegen_ts.ts +7 -8
  31. package/src/compile_package_main.ts +11 -4
  32. package/src/reception_template.ts +4 -3
  33. package/src/reception_unit_artifact.ts +105 -0
  34. package/src/reception_unit_artifact_contract.ts +220 -0
  35. package/src/reception_unit_context.ts +47 -0
  36. package/src/reception_unit_source.ts +205 -0
@@ -0,0 +1,269 @@
1
+ // Work a sync round leaves owed is never "in sync", until a round shows progress (CAP-923).
2
+ //
3
+ // Jack's iPad (TestFlight 1338, 11 Oct 2026 14:32 UTC): the Lippold estate's round ended
4
+ // `deferred=1` with "Sharing is waiting for catalogue-0744 to become resident", the next round did
5
+ // nothing (pulled nothing, head and bookmark unchanged, deferred=0), and the status went back to
6
+ // in sync while the estate was 16 commits behind its server and the phone and iPad disagreed.
7
+ import 'dart:async';
8
+
9
+ import 'package:fixnum/fixnum.dart';
10
+
11
+ import 'package:nomos_client/src/nms_generated_runtime.dart';
12
+ import 'package:nomos_types/nomos_kernel_protocol.dart' as p;
13
+ import 'package:test/test.dart';
14
+
15
+ const _estate = 'estate-0fbc5c6a931205ae404d839df6ac9ff550b3ec82';
16
+ const _ipadHead = '236fa9084f68a4baa0d00db5f8ae9587ca0d5d61';
17
+ const _serverHead = 'cb90fef930fbf2310b62e8b5080ca4e70d7a616a';
18
+ const _localEdit = '38865f0f65601093e3b0df6b56af383337f52d40';
19
+
20
+ final class _Office
21
+ with NomosRuntimeV2KernelCalls
22
+ implements PeerOfficeConnection {
23
+ final events = StreamController<p.RuntimeEvent>.broadcast(sync: true);
24
+ int sequence = 0;
25
+
26
+ @override
27
+ Stream<p.RuntimeEvent> get runtimeEvents => events.stream;
28
+
29
+ @override
30
+ int nextRuntimeRequestId() => ++sequence;
31
+
32
+ @override
33
+ Future<p.RuntimeResponse> invokeRuntime(p.RuntimeRequest request) async {
34
+ if (request.hasCloseRealm()) {
35
+ return p.RuntimeResponse(
36
+ protocolVersion: 2,
37
+ requestId: request.requestId,
38
+ realmClosed: p.RealmClosed(),
39
+ );
40
+ }
41
+ fail('Unexpected command: ${request.whichCommand()}');
42
+ }
43
+
44
+ void round(p.SyncWorkspaceResponse result) => events.add(
45
+ p.RuntimeEvent(
46
+ session: _estate,
47
+ syncCompleted: p.SyncCompleted(result: result),
48
+ ),
49
+ );
50
+
51
+ /// The 14:32:16 round: deferred on a dependency, the custodian's work not received.
52
+ void deferredRound({String head = _ipadHead, String tracked = _ipadHead}) =>
53
+ round(p.SyncWorkspaceResponse(
54
+ workspace: _estate,
55
+ previousHead: tracked,
56
+ currentHead: head,
57
+ deferred: Int64(1),
58
+ directOfferDeferred: true,
59
+ stillAhead: true,
60
+ reconnectRequired: true,
61
+ offerReceipt: p.CustodyOfferReceipt(
62
+ requestId: 'workspace-sync-9:dependency',
63
+ blocked: p.RuntimeBlocked(
64
+ kernelResidency: p.WorkspaceKernelResidencyRequired(
65
+ workspace: 'catalogue-0744dbfae089741266a9abfd9d86485bb6580a03',
66
+ detail: 'sync awaits catalogue-0744 state 51324b73',
67
+ ),
68
+ ),
69
+ ),
70
+ ));
71
+
72
+ /// The 14:32:34 round: nothing pulled, heads unchanged, nothing deferred.
73
+ void roundThatDidNothing(
74
+ {String head = _ipadHead, String tracked = _ipadHead}) =>
75
+ round(p.SyncWorkspaceResponse(
76
+ workspace: _estate,
77
+ previousHead: tracked,
78
+ currentHead: head,
79
+ ));
80
+ }
81
+
82
+ void main() {
83
+ late _Office office;
84
+ late DartReception reception;
85
+ late NomosSession session;
86
+
87
+ setUp(() {
88
+ office = _Office();
89
+ reception = DartReception(office);
90
+ session = reception.session(_estate);
91
+ });
92
+ tearDown(() async {
93
+ await reception.dispose();
94
+ await office.events.close();
95
+ });
96
+
97
+ test(
98
+ 'the iPad sequence: a deferred round, then a round that did nothing, is still not in sync',
99
+ () {
100
+ office.roundThatDidNothing();
101
+ expect(session.currentStatus.phase, SyncPhase.idle);
102
+ expect(reception.currentStatus.unsharedWork, isEmpty);
103
+
104
+ office.deferredRound();
105
+ expect(reception.currentStatus.unsharedWork.keys, [_estate]);
106
+ final owed = reception.currentStatus.unsharedWork[_estate]!;
107
+ expect(owed.outbound, isFalse,
108
+ reason: 'local main == bookmark: the owed work is the server\'s');
109
+ expect(owed.detail, contains('catalogue-0744'));
110
+ final since = owed.since;
111
+
112
+ office.roundThatDidNothing();
113
+ expect(session.currentStatus.phase, isNot(SyncPhase.idle));
114
+ expect(reception.currentStatus.phase, isNot(SyncPhase.idle));
115
+ expect(reception.currentStatus.unsharedWork.keys, [_estate]);
116
+ expect(reception.currentStatus.unsharedWork[_estate]!.since, since,
117
+ reason: 'the first sighting is kept for the attention timer');
118
+ expect(session.currentStatus.unsharedWork.keys, [_estate]);
119
+ });
120
+
121
+ test('a round that receives the server\'s work clears it', () {
122
+ office.deferredRound();
123
+ office.round(p.SyncWorkspaceResponse(
124
+ workspace: _estate,
125
+ previousHead: _serverHead,
126
+ currentHead: _serverHead,
127
+ pulled: true,
128
+ mainAdvanced: true,
129
+ ));
130
+ expect(reception.currentStatus.unsharedWork, isEmpty);
131
+ expect(session.currentStatus.phase, SyncPhase.idle);
132
+ });
133
+
134
+ test(
135
+ 'local changes not shared are outbound, and clear once the server admits them',
136
+ () {
137
+ office.deferredRound(head: _localEdit, tracked: _serverHead);
138
+ expect(reception.currentStatus.unsharedWork[_estate]!.outbound, isTrue);
139
+ office.roundThatDidNothing(head: _localEdit, tracked: _serverHead);
140
+ expect(reception.currentStatus.unsharedWork.keys, [_estate]);
141
+
142
+ office.round(p.SyncWorkspaceResponse(
143
+ workspace: _estate,
144
+ previousHead: _localEdit,
145
+ currentHead: _localEdit,
146
+ offerReceipt: p.CustodyOfferReceipt(
147
+ admission: p.CustodyOfferAdmissionReceipt(
148
+ main: _localEdit, sealedMain: _localEdit),
149
+ ),
150
+ ));
151
+ expect(reception.currentStatus.unsharedWork, isEmpty);
152
+ expect(session.currentStatus.phase, SyncPhase.idle);
153
+ });
154
+
155
+ test('admitted by the server but not yet sealed is not owed work', () {
156
+ office.round(p.SyncWorkspaceResponse(
157
+ workspace: _estate,
158
+ previousHead: _serverHead,
159
+ currentHead: _localEdit,
160
+ stillAhead: true,
161
+ offerReceipt: p.CustodyOfferReceipt(
162
+ admission: p.CustodyOfferAdmissionReceipt(main: _localEdit),
163
+ ),
164
+ ));
165
+ expect(reception.currentStatus.unsharedWork, isEmpty);
166
+ });
167
+
168
+ test('a refetch that replaced the copy clears owed work', () {
169
+ office.deferredRound();
170
+ office.round(p.SyncWorkspaceResponse(
171
+ workspace: _estate,
172
+ previousHead: _ipadHead,
173
+ currentHead: _ipadHead,
174
+ custodyReplaced: true,
175
+ ));
176
+ expect(reception.currentStatus.unsharedWork, isEmpty);
177
+ });
178
+
179
+ test(
180
+ 'a round answered "the server is opening this workspace" owes nothing: a level workspace '
181
+ 'is not left "not caught up" after the server restarts', () {
182
+ // Prod web, 12 Oct 2026: a tab loaded a minute after the cloud restarted. Four level
183
+ // workspaces' frontier probes were answered "opening"; their next rounds levelled over the
184
+ // stream and moved no head, and the popover said "haven't caught up - retrying" for ever.
185
+ office.round(p.SyncWorkspaceResponse(
186
+ workspace: _estate,
187
+ previousHead: _ipadHead,
188
+ currentHead: _ipadHead,
189
+ deferred: Int64(1),
190
+ directOfferDeferred: true,
191
+ custodianWarmingRetryAfterMillis: 2000,
192
+ ));
193
+ expect(reception.currentStatus.unsharedWork, isEmpty);
194
+ office.roundThatDidNothing();
195
+ expect(reception.currentStatus.unsharedWork, isEmpty);
196
+ expect(session.currentStatus.phase, SyncPhase.idle);
197
+ });
198
+
199
+ test('a warming round still owes local changes above the bookmark', () {
200
+ office.round(p.SyncWorkspaceResponse(
201
+ workspace: _estate,
202
+ previousHead: _serverHead,
203
+ currentHead: _localEdit,
204
+ stillAhead: true,
205
+ custodianWarmingRetryAfterMillis: 2000,
206
+ ));
207
+ expect(reception.currentStatus.unsharedWork[_estate]!.outbound, isTrue);
208
+ });
209
+
210
+ test(
211
+ 'a level copy whose server has not sealed its main yet is not "not caught up"',
212
+ () {
213
+ // The round levelled over the stream at the head the copy holds; the server's seal of that
214
+ // main is still pending, so the round reports stillAhead with equal heads and no deferral.
215
+ office.round(p.SyncWorkspaceResponse(
216
+ workspace: _estate,
217
+ previousHead: _serverHead,
218
+ currentHead: _serverHead,
219
+ stillAhead: true,
220
+ ));
221
+ expect(reception.currentStatus.unsharedWork, isEmpty);
222
+ // The same heads with a deferral are still owed work (the iPad sequence).
223
+ office.deferredRound(head: _serverHead, tracked: _serverHead);
224
+ expect(reception.currentStatus.unsharedWork.keys, [_estate]);
225
+ });
226
+
227
+ test(
228
+ 'a level round with nothing deferred clears an inbound record that no deferral set',
229
+ () {
230
+ // A round answered "another round is converging this workspace": no heads, nothing judged.
231
+ office.round(p.SyncWorkspaceResponse(
232
+ workspace: _estate,
233
+ stillAhead: true,
234
+ directOfferDeferred: true,
235
+ ));
236
+ expect(reception.currentStatus.unsharedWork[_estate]!.outbound, isFalse);
237
+ office.roundThatDidNothing();
238
+ expect(reception.currentStatus.unsharedWork, isEmpty);
239
+ expect(session.currentStatus.phase, SyncPhase.idle);
240
+
241
+ // The same at one head throughout: the record's heads never move, the level round clears it.
242
+ office.round(p.SyncWorkspaceResponse(
243
+ workspace: _estate,
244
+ previousHead: _ipadHead,
245
+ currentHead: _ipadHead,
246
+ stillAhead: true,
247
+ directOfferDeferred: true,
248
+ ));
249
+ expect(reception.currentStatus.unsharedWork.keys, [_estate]);
250
+ office.roundThatDidNothing();
251
+ expect(reception.currentStatus.unsharedWork, isEmpty);
252
+ });
253
+
254
+ test('a level round does not clear unshared local changes', () {
255
+ office.round(p.SyncWorkspaceResponse(
256
+ workspace: _estate,
257
+ previousHead: _serverHead,
258
+ currentHead: _localEdit,
259
+ stillAhead: true,
260
+ ));
261
+ expect(reception.currentStatus.unsharedWork[_estate]!.outbound, isTrue);
262
+ office.round(p.SyncWorkspaceResponse(
263
+ workspace: _estate,
264
+ previousHead: _serverHead,
265
+ currentHead: _localEdit,
266
+ ));
267
+ expect(reception.currentStatus.unsharedWork.keys, [_estate]);
268
+ });
269
+ }
@@ -1,3 +1,21 @@
1
+ ## 0.15.2
2
+
3
+ Add the shared kernel protocol fields for owned pack import continuations, cancellation and deferred admission.
4
+
5
+ - Kernel protocol: `QueryRequest.serve_last_ready`, `QueryResponse.read_index_behind` (`ReadIndexBehind`) and `RUNTIME_UNAVAILABLE_REASON_READ_INDEX_REFRESHING_SUCCESSOR_LAW` (CAP-891).
6
+
7
+ - `ReadIndexPending.lastReadyAbsent` and `LocalReadPending.heldReason` carry why a read is held for read maintenance (CAP-900).
8
+
9
+ - `NomosUnavailableReason.readIndexRefreshing` types `RUNTIME_UNAVAILABLE_REASON_READ_INDEX_REFRESHING_SUCCESSOR_LAW`, which mapped to `unknown`. The generated Dart protocol bindings now carry the CAP-891 additions (`serve_last_ready`, `read_index_behind`, reason 8); they had not been regenerated (CAP-900).
10
+
11
+ - `NomosUnavailableReason.firstDownload` types the new `RUNTIME_UNAVAILABLE_REASON_FIRST_DOWNLOAD` (CAP-913): this workspace has never been on this device and its first download is under way. A write to it is refused at once with this reason. It is distinct from `localReplicaNotReady`.
12
+
13
+ - `PeerSocketObserved.rungWorkspaces` (CAP-931): the workspaces whose upstream rang, while the socket receipt was handled, for a main this copy does not hold. Each one's follow round is due now.
14
+
15
+ ## 0.15.1
16
+
17
+ - `LiveFrontier.heldTrees`, `LiveCheckpointRequest.heldTrees`, `LiveCheckpointOffer.omittedTrees`/`sharedTrees` and `PeerStreamReceipt.omittedTrees` let a copy receive a checkpoint without the law closure it already holds (CAP-901).
18
+
1
19
  ## 0.15.0
2
20
 
3
21
  - `FollowObservationPhase.FOLLOW_OBSERVATION_PHASE_KERNEL_REFUSED`: this peer's kernel deterministically refused a replica follow's round, so the follow's sync is blocked and is not retrying.