@githolon/dsl 0.103.3 → 0.103.5
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.
- package/dart/CHANGELOG.md +15 -0
- package/dart/lib/src/runner/node-entry.mjs +411 -149
- package/dart/lib/src/runner/offline-install.mjs +222 -99
- package/dart/lib/src/session_bound.dart +111 -2
- package/dart/lib/src/test_harness.dart +117 -66
- package/dart/lib/src/webview_bridge.dart +726 -61
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +4 -5
- package/dart/test/acquire_session_test.dart +62 -5
- package/dart/test/cloud_bound_topology_test.dart +59 -38
- package/dart/test/session_bound_test.dart +25 -10
- package/dart/test/webview_bridge_test.dart +357 -9
- package/dart_types/CHANGELOG.md +5 -0
- package/dart_types/lib/nomos_types.dart +1 -0
- package/dart_types/lib/src/git_object_id.dart +37 -0
- package/dart_types/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/runtime/generated-application-host.js +425 -145
- package/src/births.ts +16 -1
- package/src/business_birth.ts +67 -46
- package/src/codegen_dart.ts +68 -12
- package/src/codegen_dart_application.ts +114 -19
- package/src/codegen_dart_client.ts +149 -6
- package/src/codegen_proof.ts +16 -9
- package/src/codegen_provider_dart.ts +5 -0
- package/src/codegen_ts.ts +115 -19
- package/src/compile_package_main.ts +142 -40
- package/src/fields.ts +43 -0
- package/src/framework/attested_receipts.ts +34 -1
- package/src/framework/domain_lifecycle.ts +140 -83
- package/src/framework/genesis.ts +2 -1
- package/src/framework/home.ts +20 -45
- package/src/framework/workspaces.ts +62 -29
- package/src/idof_lint.ts +65 -51
- package/src/index.ts +21 -1
- package/src/intent.ts +11 -1
- package/src/law_graph.ts +81 -11
- package/src/law_graph_usda.ts +45 -54
- package/src/relation_schema.ts +18 -0
- package/src/shape_compat.ts +3 -0
- package/src/stable_ids_for_module.ts +7 -1
- package/src/value_object.ts +40 -0
|
@@ -168,9 +168,30 @@ class FakeRunner implements BridgeTransport {
|
|
|
168
168
|
case 'autoSync':
|
|
169
169
|
_incoming.add(reply({'started': true}));
|
|
170
170
|
break;
|
|
171
|
+
case 'refreshCredentials':
|
|
172
|
+
_incoming.add(reply({'refreshed': true}));
|
|
173
|
+
break;
|
|
171
174
|
case 'lawStatus':
|
|
172
175
|
_incoming.add(reply({'installed': true, 'drifted': false}));
|
|
173
176
|
break;
|
|
177
|
+
case 'metrics':
|
|
178
|
+
final session = args['session'];
|
|
179
|
+
_incoming.add(reply(session == 'home'
|
|
180
|
+
? {
|
|
181
|
+
'ahead': 0,
|
|
182
|
+
'behind': false,
|
|
183
|
+
'saved': 2,
|
|
184
|
+
'onMain': 3,
|
|
185
|
+
'deadLetters': 0,
|
|
186
|
+
}
|
|
187
|
+
: {
|
|
188
|
+
'ahead': 4,
|
|
189
|
+
'behind': true,
|
|
190
|
+
'saved': 5,
|
|
191
|
+
'onMain': 6,
|
|
192
|
+
'deadLetters': 1,
|
|
193
|
+
}));
|
|
194
|
+
break;
|
|
174
195
|
case 'cryptoOwnedDeviceRequest':
|
|
175
196
|
_incoming.add(reply({'request': 'opaque-owned-device-request'}));
|
|
176
197
|
break;
|
|
@@ -225,6 +246,15 @@ class FakeRunner implements BridgeTransport {
|
|
|
225
246
|
}
|
|
226
247
|
}));
|
|
227
248
|
break;
|
|
249
|
+
case 'stageGeneratedModel':
|
|
250
|
+
_incoming.add(reply({
|
|
251
|
+
'verdict': {
|
|
252
|
+
'ok': true,
|
|
253
|
+
'status': 'pending',
|
|
254
|
+
'candidateHash': 'candidate-law-hash',
|
|
255
|
+
}
|
|
256
|
+
}));
|
|
257
|
+
break;
|
|
228
258
|
case 'restorePreviousGeneratedModel':
|
|
229
259
|
_incoming.add(reply({
|
|
230
260
|
'verdict': {
|
|
@@ -248,8 +278,11 @@ class FakeRunner implements BridgeTransport {
|
|
|
248
278
|
case 'offerDirective':
|
|
249
279
|
_incoming.add(reply({
|
|
250
280
|
'head': 'HEAD-owned-approval',
|
|
281
|
+
'intentId': 'intent-owned-approval',
|
|
251
282
|
'created': <Object?>[],
|
|
252
|
-
'
|
|
283
|
+
'born': args['directiveId'] == 'birthChild'
|
|
284
|
+
? <Object?>['notes-child']
|
|
285
|
+
: <Object?>[],
|
|
253
286
|
'workspace': 'notes',
|
|
254
287
|
}));
|
|
255
288
|
break;
|
|
@@ -300,6 +333,19 @@ class FakeRunner implements BridgeTransport {
|
|
|
300
333
|
_incoming.add({'v': kWebviewBridgeVersion, 'event': 'sync', ...event});
|
|
301
334
|
}
|
|
302
335
|
|
|
336
|
+
class ErrorRunner implements BridgeTransport {
|
|
337
|
+
final _incoming = StreamController<Map<String, Object?>>.broadcast();
|
|
338
|
+
|
|
339
|
+
@override
|
|
340
|
+
Stream<Map<String, Object?>> get messages => _incoming.stream;
|
|
341
|
+
|
|
342
|
+
@override
|
|
343
|
+
void send(Map<String, Object?> message) {}
|
|
344
|
+
|
|
345
|
+
void fail(Object error) => _incoming.addError(error);
|
|
346
|
+
Future<void> close() => _incoming.close();
|
|
347
|
+
}
|
|
348
|
+
|
|
303
349
|
/// A transport that answers EVERY request with one canned `result` (the golden corpus's reply) and records
|
|
304
350
|
/// the request the bridge sent — so we can assert both halves of the wire contract from the Dart side.
|
|
305
351
|
class ReplayTransport implements BridgeTransport {
|
|
@@ -403,6 +449,20 @@ Object? _expectedTyped(String op, Map<String, Object?> result) {
|
|
|
403
449
|
}
|
|
404
450
|
|
|
405
451
|
void main() {
|
|
452
|
+
test('transport failure fails readiness immediately', () async {
|
|
453
|
+
final transport = ErrorRunner();
|
|
454
|
+
final bridge = NomosBridge(transport);
|
|
455
|
+
final failure = StateError('runner exited');
|
|
456
|
+
transport.fail(failure);
|
|
457
|
+
await expectLater(
|
|
458
|
+
bridge.ready,
|
|
459
|
+
throwsA(isA<StateError>()
|
|
460
|
+
.having((e) => e.message, 'message', 'runner exited')),
|
|
461
|
+
);
|
|
462
|
+
await bridge.dispose();
|
|
463
|
+
await transport.close();
|
|
464
|
+
});
|
|
465
|
+
|
|
406
466
|
// ── THE GOLDEN CORPUS — the cross-language conformance the JS runner replays too ──
|
|
407
467
|
// For each case: call the typed bridge method with the corpus `args`, then assert (1) the request the bridge
|
|
408
468
|
// SENT carries exactly those args, and (2) the typed value the bridge parsed from the corpus `result` matches.
|
|
@@ -439,6 +499,7 @@ void main() {
|
|
|
439
499
|
final now = DateTime.now().millisecondsSinceEpoch;
|
|
440
500
|
final transport = ReplayTransport({
|
|
441
501
|
'head': 'H1',
|
|
502
|
+
'intentId': 'intent-span',
|
|
442
503
|
'created': <Object?>[],
|
|
443
504
|
'workspace': 'ws',
|
|
444
505
|
// the runner's op-pickup + durable-ack instants (additive `timing` field)
|
|
@@ -463,8 +524,12 @@ void main() {
|
|
|
463
524
|
test(
|
|
464
525
|
'a runner without timing (older bundle) emits no bridge-leg spans and still answers',
|
|
465
526
|
() async {
|
|
466
|
-
final transport = ReplayTransport(
|
|
467
|
-
|
|
527
|
+
final transport = ReplayTransport({
|
|
528
|
+
'head': 'H1',
|
|
529
|
+
'intentId': 'intent-old-runner',
|
|
530
|
+
'created': <Object?>[],
|
|
531
|
+
'workspace': 'ws'
|
|
532
|
+
});
|
|
468
533
|
final bridge = NomosBridge(transport);
|
|
469
534
|
await bridge.ready;
|
|
470
535
|
final out = await bridge.offerOutcome(
|
|
@@ -484,8 +549,12 @@ void main() {
|
|
|
484
549
|
// author path signs THIS offer with the intended enrolled key, not the connect-time one). Both offer lanes. ──
|
|
485
550
|
group('per-offer authorSecret threads to the runner op (#9)', () {
|
|
486
551
|
test('offerOutcome carries authorSecret to the runner op args', () async {
|
|
487
|
-
final transport = ReplayTransport(
|
|
488
|
-
|
|
552
|
+
final transport = ReplayTransport({
|
|
553
|
+
'head': 'H1',
|
|
554
|
+
'intentId': 'intent-secret',
|
|
555
|
+
'created': <Object?>[],
|
|
556
|
+
'workspace': 'ws'
|
|
557
|
+
});
|
|
489
558
|
final bridge = NomosBridge(transport);
|
|
490
559
|
await bridge.ready;
|
|
491
560
|
await bridge.offerOutcome(
|
|
@@ -520,8 +589,12 @@ void main() {
|
|
|
520
589
|
test(
|
|
521
590
|
'no authorSecret ⇒ the op omits it (byte-identical to the pre-#9 contract)',
|
|
522
591
|
() async {
|
|
523
|
-
final transport = ReplayTransport(
|
|
524
|
-
|
|
592
|
+
final transport = ReplayTransport({
|
|
593
|
+
'head': 'H1',
|
|
594
|
+
'intentId': 'intent-no-secret',
|
|
595
|
+
'created': <Object?>[],
|
|
596
|
+
'workspace': 'ws'
|
|
597
|
+
});
|
|
525
598
|
final bridge = NomosBridge(transport);
|
|
526
599
|
await bridge.ready;
|
|
527
600
|
await bridge.offerOutcome(
|
|
@@ -683,7 +756,8 @@ void main() {
|
|
|
683
756
|
await bridge.lawStatus('abc'), {'installed': true, 'drifted': false});
|
|
684
757
|
});
|
|
685
758
|
|
|
686
|
-
test(
|
|
759
|
+
test(
|
|
760
|
+
'version freshness returns the required sum type without resolving history',
|
|
687
761
|
() async {
|
|
688
762
|
await bridge.ready;
|
|
689
763
|
final reference = NmsInternalVersionedNomosRefs.create<Object, Object>(
|
|
@@ -706,7 +780,8 @@ void main() {
|
|
|
706
780
|
final request = runner.sent.last;
|
|
707
781
|
expect(request['op'], 'checkRefFreshness');
|
|
708
782
|
final args = (request['args'] as Map).cast<String, Object?>();
|
|
709
|
-
final check =
|
|
783
|
+
final check =
|
|
784
|
+
((args['refs'] as List).single as Map).cast<String, Object?>();
|
|
710
785
|
expect(check['currentField'], 'currentPublication');
|
|
711
786
|
expect((check['pinned'] as Map)['id'], 'publication:1');
|
|
712
787
|
expect((check['series'] as Map)['id'], 'series:1');
|
|
@@ -879,6 +954,26 @@ void main() {
|
|
|
879
954
|
});
|
|
880
955
|
});
|
|
881
956
|
|
|
957
|
+
test(
|
|
958
|
+
'generated model staging carries opaque USDA through one framework-private operation',
|
|
959
|
+
() async {
|
|
960
|
+
await bridge.ready;
|
|
961
|
+
await bridge.nomosInternalStageGeneratedModel(
|
|
962
|
+
domain: 'evo',
|
|
963
|
+
candidateHash: 'candidate-law-hash',
|
|
964
|
+
lawUsdz: const [1, 2, 3],
|
|
965
|
+
);
|
|
966
|
+
|
|
967
|
+
final request = runner.sent.firstWhere(
|
|
968
|
+
(message) => message['op'] == 'stageGeneratedModel',
|
|
969
|
+
);
|
|
970
|
+
expect((request['args'] as Map).cast<String, Object?>(), {
|
|
971
|
+
'domain': 'evo',
|
|
972
|
+
'candidateHash': 'candidate-law-hash',
|
|
973
|
+
'lawUsdz': 'AQID',
|
|
974
|
+
});
|
|
975
|
+
});
|
|
976
|
+
|
|
882
977
|
test(
|
|
883
978
|
'watch is a reactive stream; a runner push reaches the listener; cancel unwatches',
|
|
884
979
|
() async {
|
|
@@ -942,6 +1037,118 @@ void main() {
|
|
|
942
1037
|
expect((req['args'] as Map)['signalId'], 'signal:ws:list:main');
|
|
943
1038
|
});
|
|
944
1039
|
|
|
1040
|
+
test(
|
|
1041
|
+
'an exact write receipt advances from local acceptance to remote convergence',
|
|
1042
|
+
() async {
|
|
1043
|
+
await bridge.ready;
|
|
1044
|
+
final outcome = await bridge.offerOutcome(
|
|
1045
|
+
domain: 'notes',
|
|
1046
|
+
directiveId: 'writeNote',
|
|
1047
|
+
payload: {'text': 'hello'},
|
|
1048
|
+
);
|
|
1049
|
+
expect(outcome.receipt.current.phase, NomosOperationPhase.locallyAccepted);
|
|
1050
|
+
expect(
|
|
1051
|
+
bridge.session('notes').currentStatus.phase,
|
|
1052
|
+
SyncPhase.syncing,
|
|
1053
|
+
reason: 'a locally accepted write cannot remain falsely current',
|
|
1054
|
+
);
|
|
1055
|
+
expect(bridge.session('notes').currentStatus.ahead, 1);
|
|
1056
|
+
|
|
1057
|
+
runner.pushSync({
|
|
1058
|
+
'session': 'notes',
|
|
1059
|
+
'reason': 'write',
|
|
1060
|
+
'result': {
|
|
1061
|
+
'admittedIntentIds': ['intent-owned-approval'],
|
|
1062
|
+
},
|
|
1063
|
+
});
|
|
1064
|
+
await outcome.receipt.admitted.timeout(const Duration(seconds: 1));
|
|
1065
|
+
expect(outcome.receipt.current.phase,
|
|
1066
|
+
NomosOperationPhase.admittedToRemoteCustody);
|
|
1067
|
+
|
|
1068
|
+
runner.pushSync({
|
|
1069
|
+
'session': 'notes',
|
|
1070
|
+
'reason': 'drain-read',
|
|
1071
|
+
'result': {
|
|
1072
|
+
'convergedIntentIds': ['intent-owned-approval'],
|
|
1073
|
+
},
|
|
1074
|
+
});
|
|
1075
|
+
await outcome.receipt.converged.timeout(const Duration(seconds: 1));
|
|
1076
|
+
expect(outcome.receipt.current.isConverged, isTrue);
|
|
1077
|
+
expect(bridge.session('notes').currentStatus.phase, SyncPhase.idle);
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
test('offline debt leaves a receipt pending and reconnect settles it',
|
|
1081
|
+
() async {
|
|
1082
|
+
await bridge.ready;
|
|
1083
|
+
final outcome = await bridge.offerOutcome(
|
|
1084
|
+
domain: 'notes',
|
|
1085
|
+
directiveId: 'writeNote',
|
|
1086
|
+
payload: {'text': 'offline'},
|
|
1087
|
+
);
|
|
1088
|
+
runner.pushSync({
|
|
1089
|
+
'session': 'notes',
|
|
1090
|
+
'reason': 'write',
|
|
1091
|
+
'result': {'offline': true, 'stillAhead': true},
|
|
1092
|
+
});
|
|
1093
|
+
await Future<void>.delayed(Duration.zero);
|
|
1094
|
+
expect(outcome.receipt.current.phase, NomosOperationPhase.locallyAccepted);
|
|
1095
|
+
|
|
1096
|
+
runner.pushSync({
|
|
1097
|
+
'session': 'notes',
|
|
1098
|
+
'reason': 'reconnect',
|
|
1099
|
+
'result': {
|
|
1100
|
+
'admittedIntentIds': ['intent-owned-approval'],
|
|
1101
|
+
'convergedIntentIds': ['intent-owned-approval'],
|
|
1102
|
+
},
|
|
1103
|
+
});
|
|
1104
|
+
await outcome.receipt.converged.timeout(const Duration(seconds: 1));
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
test('a generated child workspace inherits the parent birth receipt',
|
|
1108
|
+
() async {
|
|
1109
|
+
await bridge.ready;
|
|
1110
|
+
final outcome = await bridge.offerOutcome(
|
|
1111
|
+
domain: 'notes',
|
|
1112
|
+
directiveId: 'birthChild',
|
|
1113
|
+
payload: {'name': 'child'},
|
|
1114
|
+
);
|
|
1115
|
+
final childReceipt =
|
|
1116
|
+
bridge.session('notes-child').nomosInternalWorkspaceCreationReceipt();
|
|
1117
|
+
expect(childReceipt, same(outcome.receipt));
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
test(
|
|
1121
|
+
'background authentication failure refreshes through the retained application session',
|
|
1122
|
+
() async {
|
|
1123
|
+
String token(String marker) =>
|
|
1124
|
+
'${base64Url.encode(utf8.encode('{"alg":"none"}')).replaceAll('=', '')}.'
|
|
1125
|
+
'${base64Url.encode(utf8.encode('{"sub":"alice","iss":"test","marker":"$marker"}')).replaceAll('=', '')}.x';
|
|
1126
|
+
await bridge.ready;
|
|
1127
|
+
await bridge.connect(
|
|
1128
|
+
cloud: 'https://cloud.example',
|
|
1129
|
+
workspace: 'notes',
|
|
1130
|
+
session: 'notes',
|
|
1131
|
+
authToken: token('initial'),
|
|
1132
|
+
);
|
|
1133
|
+
bridge.session('notes').nomosInternalRetainCloudAccess(
|
|
1134
|
+
cloud: 'https://cloud.example',
|
|
1135
|
+
signIn: () async => token('refreshed'),
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
runner.pushSync({
|
|
1139
|
+
'session': 'notes',
|
|
1140
|
+
'reason': 'reconnect',
|
|
1141
|
+
'result': {
|
|
1142
|
+
'pushFailed': true,
|
|
1143
|
+
'pushError': {'status': 401, 'error': 'token expired'},
|
|
1144
|
+
},
|
|
1145
|
+
});
|
|
1146
|
+
await Future<void>.delayed(const Duration(milliseconds: 10));
|
|
1147
|
+
final refresh = runner.sent
|
|
1148
|
+
.lastWhere((message) => message['op'] == 'refreshCredentials');
|
|
1149
|
+
expect((refresh['args'] as Map)['authToken'], token('refreshed'));
|
|
1150
|
+
});
|
|
1151
|
+
|
|
945
1152
|
test(
|
|
946
1153
|
'background sync status never calls an offline or failed round successful',
|
|
947
1154
|
() async {
|
|
@@ -979,6 +1186,147 @@ void main() {
|
|
|
979
1186
|
expect(bridge.currentStatus.lastError, isNull);
|
|
980
1187
|
});
|
|
981
1188
|
|
|
1189
|
+
test('application sync status scopes and folds every connected workspace',
|
|
1190
|
+
() async {
|
|
1191
|
+
await bridge.ready;
|
|
1192
|
+
await bridge.connect(cloud: 'https://c', workspace: 'home');
|
|
1193
|
+
await bridge.connect(cloud: 'https://c', workspace: 'estate');
|
|
1194
|
+
|
|
1195
|
+
final status = await bridge.syncStatus.firstWhere(
|
|
1196
|
+
(value) => value.ahead == 4 && value.behind == 1,
|
|
1197
|
+
);
|
|
1198
|
+
|
|
1199
|
+
expect(status.saved, 7);
|
|
1200
|
+
expect(status.onMain, 9);
|
|
1201
|
+
expect(status.deadLetters, 1);
|
|
1202
|
+
final metricRequests =
|
|
1203
|
+
runner.sent.where((message) => message['op'] == 'metrics').toList();
|
|
1204
|
+
expect(metricRequests, hasLength(2));
|
|
1205
|
+
expect(
|
|
1206
|
+
metricRequests
|
|
1207
|
+
.map((message) => (message['args'] as Map)['session'])
|
|
1208
|
+
.toSet(),
|
|
1209
|
+
{'home', 'estate'},
|
|
1210
|
+
);
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
test('workspace application status is isolated from sibling workspace debt',
|
|
1214
|
+
() async {
|
|
1215
|
+
await bridge.ready;
|
|
1216
|
+
await bridge.connect(cloud: 'https://c', workspace: 'home');
|
|
1217
|
+
await bridge.connect(cloud: 'https://c', workspace: 'estate');
|
|
1218
|
+
|
|
1219
|
+
expect(
|
|
1220
|
+
bridge.session('home').currentStatus.phase,
|
|
1221
|
+
SyncPhase.syncing,
|
|
1222
|
+
reason: 'an unmeasured session must not claim to be current',
|
|
1223
|
+
);
|
|
1224
|
+
|
|
1225
|
+
final home = await bridge.session('home').syncStatus.firstWhere(
|
|
1226
|
+
(value) => value.saved == 2,
|
|
1227
|
+
);
|
|
1228
|
+
final estate = await bridge.session('estate').syncStatus.firstWhere(
|
|
1229
|
+
(value) => value.saved == 5,
|
|
1230
|
+
);
|
|
1231
|
+
|
|
1232
|
+
expect(home.ahead, 0);
|
|
1233
|
+
expect(home.behind, 0);
|
|
1234
|
+
expect(estate.ahead, 4);
|
|
1235
|
+
expect(estate.behind, 1);
|
|
1236
|
+
|
|
1237
|
+
runner.pushSync({
|
|
1238
|
+
'session': 'estate',
|
|
1239
|
+
'reason': 'reconnect',
|
|
1240
|
+
'ok': false,
|
|
1241
|
+
'error': 'estate only failed',
|
|
1242
|
+
});
|
|
1243
|
+
await Future<void>.delayed(Duration.zero);
|
|
1244
|
+
expect(bridge.session('home').currentStatus.phase, SyncPhase.idle);
|
|
1245
|
+
expect(bridge.session('estate').currentStatus.phase, SyncPhase.error);
|
|
1246
|
+
expect(bridge.currentStatus.phase, SyncPhase.error);
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
test('custody push refusal is scoped, actionable, and cleared by recovery',
|
|
1250
|
+
() async {
|
|
1251
|
+
await bridge.ready;
|
|
1252
|
+
await bridge.connect(cloud: 'https://c', workspace: 'home');
|
|
1253
|
+
await bridge.connect(cloud: 'https://c', workspace: 'estate');
|
|
1254
|
+
|
|
1255
|
+
runner.pushSync({
|
|
1256
|
+
'session': 'estate',
|
|
1257
|
+
'reason': 'local-write',
|
|
1258
|
+
'result': {
|
|
1259
|
+
'stillAhead': true,
|
|
1260
|
+
'pushFailed': true,
|
|
1261
|
+
'pushError': {
|
|
1262
|
+
'status': 409,
|
|
1263
|
+
'refused': true,
|
|
1264
|
+
'error': 'candidate law is not admitted',
|
|
1265
|
+
},
|
|
1266
|
+
},
|
|
1267
|
+
});
|
|
1268
|
+
await Future<void>.delayed(Duration.zero);
|
|
1269
|
+
|
|
1270
|
+
expect(bridge.session('home').currentStatus.phase, isNot(SyncPhase.error));
|
|
1271
|
+
expect(bridge.session('estate').currentStatus.phase, SyncPhase.error);
|
|
1272
|
+
expect(
|
|
1273
|
+
bridge.session('estate').currentStatus.lastError,
|
|
1274
|
+
'Shared custody refused this workspace change (HTTP 409): '
|
|
1275
|
+
'candidate law is not admitted',
|
|
1276
|
+
);
|
|
1277
|
+
expect(bridge.currentStatus.phase, SyncPhase.error);
|
|
1278
|
+
|
|
1279
|
+
runner.pushSync({
|
|
1280
|
+
'session': 'estate',
|
|
1281
|
+
'reason': 'retry',
|
|
1282
|
+
'result': {'stillAhead': false},
|
|
1283
|
+
});
|
|
1284
|
+
await Future<void>.delayed(Duration.zero);
|
|
1285
|
+
|
|
1286
|
+
expect(bridge.session('estate').currentStatus.phase, SyncPhase.idle);
|
|
1287
|
+
expect(bridge.session('estate').currentStatus.lastError, isNull);
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
test('status listener attached before connect never sends ambiguous metrics',
|
|
1291
|
+
() async {
|
|
1292
|
+
await bridge.ready;
|
|
1293
|
+
final statuses = <SyncStatus>[];
|
|
1294
|
+
final subscription = bridge.syncStatus.listen(statuses.add);
|
|
1295
|
+
await Future<void>.delayed(Duration.zero);
|
|
1296
|
+
|
|
1297
|
+
expect(
|
|
1298
|
+
runner.sent.where((message) => message['op'] == 'metrics'),
|
|
1299
|
+
isEmpty,
|
|
1300
|
+
);
|
|
1301
|
+
|
|
1302
|
+
await bridge.connect(cloud: 'https://c', workspace: 'home');
|
|
1303
|
+
await bridge.connect(cloud: 'https://c', workspace: 'estate');
|
|
1304
|
+
for (var attempt = 0;
|
|
1305
|
+
attempt < 20 &&
|
|
1306
|
+
!statuses.any((status) => status.ahead == 4 && status.behind == 1);
|
|
1307
|
+
attempt += 1) {
|
|
1308
|
+
await Future<void>.delayed(const Duration(milliseconds: 10));
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
final metricRequests =
|
|
1312
|
+
runner.sent.where((message) => message['op'] == 'metrics').toList();
|
|
1313
|
+
expect(metricRequests, isNotEmpty);
|
|
1314
|
+
expect(
|
|
1315
|
+
metricRequests.every(
|
|
1316
|
+
(message) =>
|
|
1317
|
+
(message['args'] as Map)['session'] is String &&
|
|
1318
|
+
((message['args'] as Map)['session'] as String).isNotEmpty,
|
|
1319
|
+
),
|
|
1320
|
+
isTrue,
|
|
1321
|
+
);
|
|
1322
|
+
expect(
|
|
1323
|
+
statuses.any((status) => status.ahead == 4 && status.behind == 1),
|
|
1324
|
+
isTrue,
|
|
1325
|
+
);
|
|
1326
|
+
expect(statuses.last.lastError, isNull);
|
|
1327
|
+
await subscription.cancel();
|
|
1328
|
+
});
|
|
1329
|
+
|
|
982
1330
|
test('a law refusal throws NomosBridgeError with the verbatim message',
|
|
983
1331
|
() async {
|
|
984
1332
|
await bridge.ready;
|
package/dart_types/CHANGELOG.md
CHANGED
|
@@ -46,6 +46,7 @@ export 'src/invitation.dart' show Identity, User, Recipient, Invitation;
|
|
|
46
46
|
// birth methods — actor/authorSecret are distinct types, so a transposition is a compile error.
|
|
47
47
|
export 'src/principal.dart';
|
|
48
48
|
export 'src/sha256_digest.dart';
|
|
49
|
+
export 'src/git_object_id.dart';
|
|
49
50
|
// The CUSTODY-LIFECYCLE types (rebirth_and_custody_lifecycle.md): NomosCustodyBreak / NomosCustodyStatus /
|
|
50
51
|
// CustodyBreakPolicy — custody incompatibility as a first-class, typed, app-decidable event. Pure data.
|
|
51
52
|
export 'src/custody.dart';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth.
|
|
3
|
+
|
|
4
|
+
/// A Git object identity in Nomos's current gix custody spelling: exactly 40
|
|
5
|
+
/// lowercase hexadecimal characters.
|
|
6
|
+
///
|
|
7
|
+
/// This is deliberately not interchangeable with [String] or a semantic
|
|
8
|
+
/// content digest. The wrapper is zero-cost at runtime and validates at the
|
|
9
|
+
/// application boundary before an offer is assembled.
|
|
10
|
+
extension type const GitObjectId._(String value) {
|
|
11
|
+
factory GitObjectId(String value) {
|
|
12
|
+
if (!_isLowercaseGitObjectId(value)) {
|
|
13
|
+
throw FormatException(
|
|
14
|
+
'expected a 40-character lowercase hexadecimal Git object id',
|
|
15
|
+
value,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return GitObjectId._(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static GitObjectId parse(String value) => GitObjectId(value);
|
|
22
|
+
|
|
23
|
+
static GitObjectId? tryParse(String value) {
|
|
24
|
+
if (!_isLowercaseGitObjectId(value)) return null;
|
|
25
|
+
return GitObjectId._(value);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
bool _isLowercaseGitObjectId(String value) {
|
|
30
|
+
if (value.length != 40) return false;
|
|
31
|
+
for (final codeUnit in value.codeUnits) {
|
|
32
|
+
final digit = codeUnit >= 0x30 && codeUnit <= 0x39;
|
|
33
|
+
final lowercaseHex = codeUnit >= 0x61 && codeUnit <= 0x66;
|
|
34
|
+
if (!digit && !lowercaseHex) return false;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
package/dart_types/pubspec.yaml
CHANGED
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
Pure kernel wire/data types for Nomos Dart frontends — no transport, no bridge,
|
|
10
10
|
no runtime. The hand-written serde-JSON-compatible types plus the offer codec,
|
|
11
11
|
reactive read engine, schema validation, and data-only error/trace types.
|
|
12
|
-
version: 0.8.
|
|
12
|
+
version: 0.8.3
|
|
13
13
|
|
|
14
14
|
environment:
|
|
15
15
|
sdk: ^3.5.0
|
package/package.json
CHANGED