@githolon/dsl 0.103.4 → 0.103.6
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 +8 -0
- package/dart/lib/src/runner/node-entry.mjs +333 -117
- package/dart/lib/src/runner/offline-install.mjs +173 -67
- package/dart/lib/src/session_bound.dart +111 -2
- package/dart/lib/src/test_harness.dart +51 -41
- package/dart/lib/src/webview_bridge.dart +714 -60
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- 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 +326 -7
- 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 +347 -113
- package/src/births.ts +16 -1
- package/src/business_birth.ts +55 -34
- 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 +159 -43
- package/src/fields.ts +43 -0
- package/src/framework/domain_lifecycle.ts +108 -88
- package/src/framework/genesis.ts +2 -1
- package/src/framework/home.ts +6 -43
- package/src/framework/workspaces.ts +42 -25
- 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/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;
|
|
@@ -466,6 +499,7 @@ void main() {
|
|
|
466
499
|
final now = DateTime.now().millisecondsSinceEpoch;
|
|
467
500
|
final transport = ReplayTransport({
|
|
468
501
|
'head': 'H1',
|
|
502
|
+
'intentId': 'intent-span',
|
|
469
503
|
'created': <Object?>[],
|
|
470
504
|
'workspace': 'ws',
|
|
471
505
|
// the runner's op-pickup + durable-ack instants (additive `timing` field)
|
|
@@ -490,8 +524,12 @@ void main() {
|
|
|
490
524
|
test(
|
|
491
525
|
'a runner without timing (older bundle) emits no bridge-leg spans and still answers',
|
|
492
526
|
() async {
|
|
493
|
-
final transport = ReplayTransport(
|
|
494
|
-
|
|
527
|
+
final transport = ReplayTransport({
|
|
528
|
+
'head': 'H1',
|
|
529
|
+
'intentId': 'intent-old-runner',
|
|
530
|
+
'created': <Object?>[],
|
|
531
|
+
'workspace': 'ws'
|
|
532
|
+
});
|
|
495
533
|
final bridge = NomosBridge(transport);
|
|
496
534
|
await bridge.ready;
|
|
497
535
|
final out = await bridge.offerOutcome(
|
|
@@ -511,8 +549,12 @@ void main() {
|
|
|
511
549
|
// author path signs THIS offer with the intended enrolled key, not the connect-time one). Both offer lanes. ──
|
|
512
550
|
group('per-offer authorSecret threads to the runner op (#9)', () {
|
|
513
551
|
test('offerOutcome carries authorSecret to the runner op args', () async {
|
|
514
|
-
final transport = ReplayTransport(
|
|
515
|
-
|
|
552
|
+
final transport = ReplayTransport({
|
|
553
|
+
'head': 'H1',
|
|
554
|
+
'intentId': 'intent-secret',
|
|
555
|
+
'created': <Object?>[],
|
|
556
|
+
'workspace': 'ws'
|
|
557
|
+
});
|
|
516
558
|
final bridge = NomosBridge(transport);
|
|
517
559
|
await bridge.ready;
|
|
518
560
|
await bridge.offerOutcome(
|
|
@@ -547,8 +589,12 @@ void main() {
|
|
|
547
589
|
test(
|
|
548
590
|
'no authorSecret ⇒ the op omits it (byte-identical to the pre-#9 contract)',
|
|
549
591
|
() async {
|
|
550
|
-
final transport = ReplayTransport(
|
|
551
|
-
|
|
592
|
+
final transport = ReplayTransport({
|
|
593
|
+
'head': 'H1',
|
|
594
|
+
'intentId': 'intent-no-secret',
|
|
595
|
+
'created': <Object?>[],
|
|
596
|
+
'workspace': 'ws'
|
|
597
|
+
});
|
|
552
598
|
final bridge = NomosBridge(transport);
|
|
553
599
|
await bridge.ready;
|
|
554
600
|
await bridge.offerOutcome(
|
|
@@ -908,6 +954,26 @@ void main() {
|
|
|
908
954
|
});
|
|
909
955
|
});
|
|
910
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
|
+
|
|
911
977
|
test(
|
|
912
978
|
'watch is a reactive stream; a runner push reaches the listener; cancel unwatches',
|
|
913
979
|
() async {
|
|
@@ -971,6 +1037,118 @@ void main() {
|
|
|
971
1037
|
expect((req['args'] as Map)['signalId'], 'signal:ws:list:main');
|
|
972
1038
|
});
|
|
973
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
|
+
|
|
974
1152
|
test(
|
|
975
1153
|
'background sync status never calls an offline or failed round successful',
|
|
976
1154
|
() async {
|
|
@@ -1008,6 +1186,147 @@ void main() {
|
|
|
1008
1186
|
expect(bridge.currentStatus.lastError, isNull);
|
|
1009
1187
|
});
|
|
1010
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
|
+
|
|
1011
1330
|
test('a law refusal throws NomosBridgeError with the verbatim message',
|
|
1012
1331
|
() async {
|
|
1013
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