@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.
Files changed (40) hide show
  1. package/dart/CHANGELOG.md +8 -0
  2. package/dart/lib/src/runner/node-entry.mjs +333 -117
  3. package/dart/lib/src/runner/offline-install.mjs +173 -67
  4. package/dart/lib/src/session_bound.dart +111 -2
  5. package/dart/lib/src/test_harness.dart +51 -41
  6. package/dart/lib/src/webview_bridge.dart +714 -60
  7. package/dart/pubspec.lock +1 -1
  8. package/dart/pubspec.yaml +2 -2
  9. package/dart/test/acquire_session_test.dart +62 -5
  10. package/dart/test/cloud_bound_topology_test.dart +59 -38
  11. package/dart/test/session_bound_test.dart +25 -10
  12. package/dart/test/webview_bridge_test.dart +326 -7
  13. package/dart_types/CHANGELOG.md +5 -0
  14. package/dart_types/lib/nomos_types.dart +1 -0
  15. package/dart_types/lib/src/git_object_id.dart +37 -0
  16. package/dart_types/pubspec.yaml +1 -1
  17. package/package.json +1 -1
  18. package/runtime/generated-application-host.js +347 -113
  19. package/src/births.ts +16 -1
  20. package/src/business_birth.ts +55 -34
  21. package/src/codegen_dart.ts +68 -12
  22. package/src/codegen_dart_application.ts +114 -19
  23. package/src/codegen_dart_client.ts +149 -6
  24. package/src/codegen_proof.ts +16 -9
  25. package/src/codegen_provider_dart.ts +5 -0
  26. package/src/codegen_ts.ts +115 -19
  27. package/src/compile_package_main.ts +159 -43
  28. package/src/fields.ts +43 -0
  29. package/src/framework/domain_lifecycle.ts +108 -88
  30. package/src/framework/genesis.ts +2 -1
  31. package/src/framework/home.ts +6 -43
  32. package/src/framework/workspaces.ts +42 -25
  33. package/src/idof_lint.ts +65 -51
  34. package/src/index.ts +21 -1
  35. package/src/intent.ts +11 -1
  36. package/src/law_graph.ts +81 -11
  37. package/src/relation_schema.ts +18 -0
  38. package/src/shape_compat.ts +3 -0
  39. package/src/stable_ids_for_module.ts +7 -1
  40. 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
- 'births': <Object?>[],
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
- {'head': 'H1', 'created': <Object?>[], 'workspace': 'ws'});
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
- {'head': 'H1', 'created': <Object?>[], 'workspace': 'ws'});
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
- {'head': 'H1', 'created': <Object?>[], 'workspace': 'ws'});
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;
@@ -1,3 +1,8 @@
1
+ ## 0.8.3
2
+
3
+ - Add the typed `GitObjectId` value used to identify immutable custody objects without falling back to
4
+ interchangeable strings.
5
+
1
6
  ## 0.8.2
2
7
 
3
8
  - Carry the typed bounded-startup, missing-law and governed child-policy errors used by the USDA-native
@@ -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
+ }
@@ -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.2
12
+ version: 0.8.3
13
13
 
14
14
  environment:
15
15
  sdk: ^3.5.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.103.4",
3
+ "version": "0.103.6",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",