@githolon/dsl 0.70.2 → 0.72.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.
- package/dart/CHANGELOG.md +13 -0
- package/dart/lib/nomos_client.dart +4 -0
- package/dart/lib/src/session_bound.dart +309 -0
- package/dart/lib/src/test_harness.dart +136 -4
- package/dart/lib/src/webview_bridge.dart +173 -27
- package/dart/pubspec.yaml +1 -1
- package/dart/test/cluster_attested_test.dart +0 -1
- package/dart/test/session_bound_test.dart +255 -0
- package/dart_types/CHANGELOG.md +8 -0
- package/dart_types/lib/nomos_types.dart +6 -0
- package/dart_types/lib/src/principal.dart +56 -0
- package/dart_types/lib/src/surfaced.dart +208 -0
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +69 -0
- package/package.json +1 -1
- package/src/aggregate.ts +64 -7
- package/src/build_package.ts +22 -0
- package/src/codegen_dart.ts +155 -31
- package/src/codegen_dart_client.ts +274 -15
- package/src/codegen_openapi.ts +20 -0
- package/src/codegen_proof.ts +5 -1
- package/src/codegen_ts.ts +166 -1
- package/src/compile_package_main.ts +126 -9
- package/src/directive.ts +40 -0
- package/src/drivers.ts +63 -2
- package/src/idof_lint.ts +144 -0
- package/src/index.ts +1 -0
- package/src/surfaced_lint.ts +0 -0
- package/src/usd_layers.ts +5 -0
- package/src/wire.ts +4 -1
|
@@ -31,6 +31,15 @@ import 'package:nomos_types/nomos_types.dart'
|
|
|
31
31
|
NomosCustodyEvent,
|
|
32
32
|
NomosCustodyStatus;
|
|
33
33
|
|
|
34
|
+
import 'session_bound.dart' show DomainNotInstalledForSession;
|
|
35
|
+
|
|
36
|
+
/// THE ENGINE (architecture/session_bound_clients.md): the multi-workspace RUNTIME — mount/connect,
|
|
37
|
+
/// the session registry, sync, custody + recovery, the home ceremony, and [NomosBridge.follow]
|
|
38
|
+
/// (ref → mounted session). NOT the surface for domain writes: those live on the generated
|
|
39
|
+
/// `<Domain>Client` bound to a [NomosSession]. `NomosEngine` is the doctrine-named alias of the
|
|
40
|
+
/// bridge root so app code reads as the four-layer split (engine → session → client → values).
|
|
41
|
+
typedef NomosEngine = NomosBridge;
|
|
42
|
+
|
|
34
43
|
/// The protocol version the runner and the bridge agree on (`webview-runner.mjs` `WEBVIEW_BRIDGE_VERSION`).
|
|
35
44
|
const int kWebviewBridgeVersion = 1;
|
|
36
45
|
|
|
@@ -523,7 +532,7 @@ class NomosEncryption {
|
|
|
523
532
|
});
|
|
524
533
|
}
|
|
525
534
|
final epoch = (await _currentEpoch(scope)) + 1; // forward-only: strictly increments
|
|
526
|
-
final head = await bridge.
|
|
535
|
+
final head = (await bridge.offerOutcome(
|
|
527
536
|
domain: domain,
|
|
528
537
|
directiveId: 'advanceKeyEpoch',
|
|
529
538
|
payload: {
|
|
@@ -534,7 +543,8 @@ class NomosEncryption {
|
|
|
534
543
|
},
|
|
535
544
|
actor: actor,
|
|
536
545
|
domainHash: domainHash,
|
|
537
|
-
)
|
|
546
|
+
))
|
|
547
|
+
.head;
|
|
538
548
|
return NomosEpochResult(head: head, epoch: epoch, roster: members);
|
|
539
549
|
}
|
|
540
550
|
|
|
@@ -553,7 +563,7 @@ class NomosEncryption {
|
|
|
553
563
|
String? openedAt,
|
|
554
564
|
}) async {
|
|
555
565
|
final s = _defScope(scope);
|
|
556
|
-
await bridge.
|
|
566
|
+
await bridge.offerOutcome(
|
|
557
567
|
domain: domain,
|
|
558
568
|
directiveId: grantDirective,
|
|
559
569
|
payload: grantPayload,
|
|
@@ -581,7 +591,7 @@ class NomosEncryption {
|
|
|
581
591
|
String? openedAt,
|
|
582
592
|
}) async {
|
|
583
593
|
final s = _defScope(scope);
|
|
584
|
-
await bridge.
|
|
594
|
+
await bridge.offerOutcome(
|
|
585
595
|
domain: domain,
|
|
586
596
|
directiveId: revokeDirective,
|
|
587
597
|
payload: revokePayload,
|
|
@@ -629,13 +639,26 @@ class NomosOfferOutcome {
|
|
|
629
639
|
/// The raw per-child birth outcomes the holon reported (`{ workspace, genesisHead, … }`), when present.
|
|
630
640
|
final List<Map<String, Object?>> birthOutcomes;
|
|
631
641
|
|
|
642
|
+
/// LEGACY GATEWAY LANE ONLY: the parent gateway's verdict, when this outcome wraps a deprecated
|
|
643
|
+
/// cross-workspace `createWorkspace` relay (the old generated-birth shape). Null on the one
|
|
644
|
+
/// local offer lane.
|
|
645
|
+
final Map<String, Object?>? gatewayVerdict;
|
|
646
|
+
|
|
632
647
|
const NomosOfferOutcome({
|
|
633
648
|
required this.head,
|
|
634
649
|
required this.created,
|
|
635
650
|
this.workspace = '',
|
|
636
651
|
this.born = const [],
|
|
637
652
|
this.birthOutcomes = const [],
|
|
653
|
+
this.gatewayVerdict,
|
|
638
654
|
});
|
|
655
|
+
|
|
656
|
+
/// The born child workspace(s) as followable [NomosRef]s — hand one to [NomosBridge.follow]
|
|
657
|
+
/// (`final estate = await engine.follow(outcome.bornRefs.single);`) so app code never carries a
|
|
658
|
+
/// loose workspace string. The ref's type is the reserved `nomos.Workspace` locator tag.
|
|
659
|
+
List<NomosRef> get bornRefs => born
|
|
660
|
+
.map((w) => NomosRef(workspace: w, type: 'nomos.Workspace', id: w))
|
|
661
|
+
.toList();
|
|
639
662
|
}
|
|
640
663
|
|
|
641
664
|
/// THE BRIDGE. Construct over a [BridgeTransport], `await ready`, then `connect(...)` and drive the holon.
|
|
@@ -985,6 +1008,9 @@ class NomosBridge {
|
|
|
985
1008
|
Map<String, Object?>? _lastRecovery;
|
|
986
1009
|
|
|
987
1010
|
/// LOCAL-FIRST offer: carry opaque intent bytes into the GitHolon. Returns the new local head; durable on [sync].
|
|
1011
|
+
@Deprecated('Engine-level generic writes are the footgun (session_bound_clients.md ask 1/7): '
|
|
1012
|
+
'dispatch through a generated <Domain>Client bound to a NomosSession '
|
|
1013
|
+
'(XClient.bind(session)). Retained for CLIs/admin tooling/generated internals only.')
|
|
988
1014
|
Future<String> offer({required String intentBytes}) async {
|
|
989
1015
|
final r = _asMap(await _call('offer', {'intentBytes': intentBytes}));
|
|
990
1016
|
return r['head'].toString();
|
|
@@ -1000,6 +1026,10 @@ class NomosBridge {
|
|
|
1000
1026
|
/// its trust policy); [force] carries the deploy/evolve force flag through. Both are OMITTED from the wire
|
|
1001
1027
|
/// when absent, so a plain offer is byte-identical to before. [session] pins the colocated workspace in a
|
|
1002
1028
|
/// cluster harness (absent ⇒ the runner's sole-session default).
|
|
1029
|
+
@Deprecated('Engine-level string-y writes are the footgun (session_bound_clients.md ask 1/7): '
|
|
1030
|
+
'dispatch through a generated <Domain>Client bound to a NomosSession '
|
|
1031
|
+
'(XClient.bind(session) — no domain/directive strings at call sites). '
|
|
1032
|
+
'Retained for CLIs/admin tooling/generated internals only.')
|
|
1003
1033
|
Future<String> offerDirective({
|
|
1004
1034
|
required String domain,
|
|
1005
1035
|
required String directiveId,
|
|
@@ -1027,6 +1057,9 @@ class NomosBridge {
|
|
|
1027
1057
|
/// inside the sealed intent (callers never supply one); the runner hands back what was minted, so a
|
|
1028
1058
|
/// generated create method returns the typed id with NO query-after round-trip. [created] is empty for
|
|
1029
1059
|
/// pure mutations.
|
|
1060
|
+
@Deprecated('Engine-level string-y writes are the footgun (session_bound_clients.md ask 1/7): '
|
|
1061
|
+
'dispatch through a generated <Domain>Client bound to a NomosSession. '
|
|
1062
|
+
'Retained for CLIs/admin tooling/generated internals only.')
|
|
1030
1063
|
Future<NomosOfferOutcome> offerCreates({
|
|
1031
1064
|
required String domain,
|
|
1032
1065
|
required String directiveId,
|
|
@@ -1063,16 +1096,22 @@ class NomosBridge {
|
|
|
1063
1096
|
bool? force,
|
|
1064
1097
|
String? session,
|
|
1065
1098
|
}) async {
|
|
1066
|
-
final
|
|
1067
|
-
|
|
1068
|
-
'
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1099
|
+
final Map<String, Object?> r;
|
|
1100
|
+
try {
|
|
1101
|
+
r = _asMap(await _call('offerDirective', {
|
|
1102
|
+
'domain': domain,
|
|
1103
|
+
'directiveId': directiveId,
|
|
1104
|
+
'payload': payload,
|
|
1105
|
+
if (actor != null) 'actor': actor,
|
|
1106
|
+
if (domainHash != null) 'domainHash': domainHash,
|
|
1107
|
+
if (attestations != null) 'attestations': attestations,
|
|
1108
|
+
if (force != null) 'force': force,
|
|
1109
|
+
if (session != null) 'session': session,
|
|
1110
|
+
}));
|
|
1111
|
+
} catch (e) {
|
|
1112
|
+
throw await _wrapDomainNotInstalled(e,
|
|
1113
|
+
domain: domain, domainHash: domainHash, requestedWorkspace: session);
|
|
1114
|
+
}
|
|
1076
1115
|
final births = ((r['birthOutcomes'] as List?) ?? const [])
|
|
1077
1116
|
.whereType<Map>()
|
|
1078
1117
|
.map((m) => m.cast<String, Object?>())
|
|
@@ -1106,6 +1145,66 @@ class NomosBridge {
|
|
|
1106
1145
|
);
|
|
1107
1146
|
}
|
|
1108
1147
|
|
|
1148
|
+
/// CONTEXT-RICH "domain not installed" (session_bound_clients.md ask 6): the kernel's generic
|
|
1149
|
+
/// `no active installed law declares domain 'x'` refusal is WRAPPED here with the session
|
|
1150
|
+
/// context (which session, what it actually runs, the likely causes) — the string itself
|
|
1151
|
+
/// originates in the sealed kernel, so the bridge (not the kernel) owns the ergonomics.
|
|
1152
|
+
/// Anything that isn't that refusal passes through unchanged.
|
|
1153
|
+
Future<Object> _wrapDomainNotInstalled(Object e,
|
|
1154
|
+
{required String domain, String? domainHash, String? requestedWorkspace}) async {
|
|
1155
|
+
if (!RegExp(r"no active installed law declares domain '").hasMatch(e.toString())) return e;
|
|
1156
|
+
final self = this;
|
|
1157
|
+
final sessionWs = self is NomosSession ? self.workspace : null;
|
|
1158
|
+
var activeKeys = const <String>[];
|
|
1159
|
+
try {
|
|
1160
|
+
final offer = await interfaceOffer();
|
|
1161
|
+
if (offer != null) activeKeys = offer.keys.toList()..sort();
|
|
1162
|
+
} catch (_) {/* best-effort enrichment — the typed error still carries the context we have */}
|
|
1163
|
+
return DomainNotInstalledForSession(
|
|
1164
|
+
expectedDomainKey: domain,
|
|
1165
|
+
expectedDomainHash: domainHash,
|
|
1166
|
+
sessionId: requestedWorkspace ?? sessionWs,
|
|
1167
|
+
sessionWorkspace: sessionWs,
|
|
1168
|
+
requestedWorkspace: requestedWorkspace ?? sessionWs,
|
|
1169
|
+
activeDomainKeys: activeKeys,
|
|
1170
|
+
activeDomainHashes: compat?.workspaceRuns ?? const [],
|
|
1171
|
+
underlying: e,
|
|
1172
|
+
);
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/// THE WORKSPACE'S ON-LEDGER STRUCTURAL OFFER (#72): the per-domain interface embedded in each
|
|
1176
|
+
/// installed package, read off the LOCAL projection (offline). Keys = the ACTIVE domain keys the
|
|
1177
|
+
/// workspace serves. Null when no on-ledger interface is observed yet (pre-#72e law).
|
|
1178
|
+
Future<Map<String, Object?>?> interfaceOffer({String? session}) async {
|
|
1179
|
+
final r = _asMap(await _call('interfaceOffer', {if (session != null) 'session': session}));
|
|
1180
|
+
return (r['offer'] as Map?)?.cast<String, Object?>();
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/// FOLLOW A [NomosRef] — THE cross-workspace primitive (session_bound_clients.md ask 8): mount
|
|
1184
|
+
/// the referenced workspace on this device's plane (idempotent — an already-open workspace is
|
|
1185
|
+
/// just returned) and hand back its [NomosSession]. Typed client construction is then one line:
|
|
1186
|
+
/// `final estate = await engine.follow(outcome.bornRefs.single); final app = await EstateClient.bind(estate);`
|
|
1187
|
+
/// No loose `{workspaceId, aggregateId}` string pairs in app code. [cloud] binds the mount to a
|
|
1188
|
+
/// cloud (omit for the local plane — a locally-born child mounts in place, no re-clone).
|
|
1189
|
+
Future<NomosSession> follow(NomosRef ref,
|
|
1190
|
+
{String? cloud, String? clientId, String? authToken, bool allowUnborn = false}) async {
|
|
1191
|
+
final ws = ref.workspace;
|
|
1192
|
+
if (ws.isEmpty) {
|
|
1193
|
+
throw ArgumentError.value(ref, 'ref',
|
|
1194
|
+
'follow: the NomosRef carries no workspace — only a workspace-complete ref is followable');
|
|
1195
|
+
}
|
|
1196
|
+
if (!_sessionNames.contains(ws)) {
|
|
1197
|
+
await connect(
|
|
1198
|
+
cloud: cloud,
|
|
1199
|
+
workspace: ws,
|
|
1200
|
+
session: ws,
|
|
1201
|
+
clientId: clientId,
|
|
1202
|
+
authToken: authToken,
|
|
1203
|
+
allowUnborn: allowUnborn);
|
|
1204
|
+
}
|
|
1205
|
+
return session(ws);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1109
1208
|
/// Reserve a KERNEL-MINTED typed id (`<TypeTag>_<uuidv7>`) via the holon's front-door mint — for app-side
|
|
1110
1209
|
/// references (routing keys, optimistic UI). `.creates` payloads still carry NO id: the LAW mints those
|
|
1111
1210
|
/// (see [offerCreates], which returns them).
|
|
@@ -1121,6 +1220,9 @@ class NomosBridge {
|
|
|
1121
1220
|
/// OFFER AN INTENT — the write verb. Plan a directive (domain/directiveId/payload) as the verified [actor];
|
|
1122
1221
|
/// the holon encodes the v2 offer envelope via the single codec and admits it. Returns the new local head.
|
|
1123
1222
|
/// (This is the canonical name; [offerDirective] is the legacy alias the generated clients still call.)
|
|
1223
|
+
@Deprecated('Engine-level string-y writes are the footgun (session_bound_clients.md ask 1/7): '
|
|
1224
|
+
'dispatch through a generated <Domain>Client bound to a NomosSession. '
|
|
1225
|
+
'Retained for CLIs/admin tooling/generated internals only.')
|
|
1124
1226
|
Future<String> offerIntent({
|
|
1125
1227
|
required String domain,
|
|
1126
1228
|
required String directiveId,
|
|
@@ -1169,10 +1271,22 @@ class NomosBridge {
|
|
|
1169
1271
|
}));
|
|
1170
1272
|
|
|
1171
1273
|
/// MAKE A WORKSPACE — the one-call tenant path. A child workspace under a platform/[parent] is a SIGNED
|
|
1172
|
-
/// control-plane offer; the holon seals `createWorkspace` (or [directiveId] `birthChild
|
|
1173
|
-
///
|
|
1174
|
-
///
|
|
1274
|
+
/// control-plane offer; the holon seals `createWorkspace` (or [directiveId] `birthChild`, or ANY
|
|
1275
|
+
/// tenant-defined `.births()` directive — see [domain]) with the requester's device key ([authorSecret],
|
|
1276
|
+
/// as [actor], e.g. `user:<uid>`) and relays it to the parent's gateway. Returns the worker verdict
|
|
1277
|
+
/// (`{ok, status, body}`). The tenant move is one line:
|
|
1175
1278
|
/// `await bridge.createWorkspace(name: 'foo', parent: 'my-platform', actor: 'user:$uid', authorSecret: key)`.
|
|
1279
|
+
///
|
|
1280
|
+
/// [domain] defaults to `'workspaces'` (the generic root-style governance law every platform's
|
|
1281
|
+
/// createWorkspace/birthChild rides). To birth through the PARENT'S OWN custom platform law instead (e.g.
|
|
1282
|
+
/// a tenant `birthEstateWorkspace` under a `co2_platform` domain), pass that domain's key + [domainHash]:
|
|
1283
|
+
/// `bridge.createWorkspace(domain: 'co2_platform', directiveId: 'birthEstateWorkspace', domainHash: ...)`.
|
|
1284
|
+
/// The kernel looks up `(domain, directiveId)` in the ACTIVE installed law — [domain] must name the law
|
|
1285
|
+
/// that actually declares [directiveId], never assume `'workspaces'`.
|
|
1286
|
+
@Deprecated('Engine-level generic writes with a loose parent string are the footgun '
|
|
1287
|
+
'(session_bound_clients.md asks 1/4/7): births go through the generated <Domain>Client '
|
|
1288
|
+
'bound to the PARENT NomosSession (its birth method infers parent = the session workspace). '
|
|
1289
|
+
'Retained for CLIs/admin tooling/generated compatibility shims only.')
|
|
1176
1290
|
Future<Map<String, Object?>> createWorkspace({
|
|
1177
1291
|
required String parent,
|
|
1178
1292
|
required String actor,
|
|
@@ -1180,21 +1294,29 @@ class NomosBridge {
|
|
|
1180
1294
|
String? name,
|
|
1181
1295
|
Object? payload,
|
|
1182
1296
|
String directiveId = 'createWorkspace',
|
|
1297
|
+
String domain = 'workspaces',
|
|
1183
1298
|
String? domainHash,
|
|
1184
1299
|
String? authToken,
|
|
1185
1300
|
}) async {
|
|
1186
1301
|
// [authorSecret] defaults to the connection's signing key (the home key NomosScope.home loaded) —
|
|
1187
1302
|
// a frontend dev never threads a key through.
|
|
1188
|
-
final
|
|
1189
|
-
|
|
1190
|
-
'
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1303
|
+
final Map<String, Object?> r;
|
|
1304
|
+
try {
|
|
1305
|
+
r = _asMap(await _call('createWorkspace', {
|
|
1306
|
+
'parent': parent,
|
|
1307
|
+
'actor': actor,
|
|
1308
|
+
if (authorSecret != null) 'authorSecret': authorSecret,
|
|
1309
|
+
if (name != null) 'name': name,
|
|
1310
|
+
if (payload != null) 'payload': payload,
|
|
1311
|
+
'directiveId': directiveId,
|
|
1312
|
+
'domain': domain,
|
|
1313
|
+
if (domainHash != null) 'domainHash': domainHash,
|
|
1314
|
+
if (authToken != null) 'authToken': authToken,
|
|
1315
|
+
}));
|
|
1316
|
+
} catch (e) {
|
|
1317
|
+
throw await _wrapDomainNotInstalled(e,
|
|
1318
|
+
domain: domain, domainHash: domainHash, requestedWorkspace: parent);
|
|
1319
|
+
}
|
|
1198
1320
|
return (r['verdict'] as Map?)?.cast<String, Object?>() ?? r;
|
|
1199
1321
|
}
|
|
1200
1322
|
|
|
@@ -1459,6 +1581,18 @@ class NomosBridge {
|
|
|
1459
1581
|
}))['sum']) as num)
|
|
1460
1582
|
.toInt();
|
|
1461
1583
|
|
|
1584
|
+
/// THE PENDING-CONFLICTS READ (Surfaced merge conflicts — the DX layer): the MAINTAINED,
|
|
1585
|
+
/// workspace-wide "decisions needed" index (reserved id `nomosPendingConflicts`). One row per
|
|
1586
|
+
/// (aggregate, field) whose folded state carries a Surfaced pending set of 2+ observations —
|
|
1587
|
+
/// `{aggregateType, aggregateId, field, observations: [{value, author, occurredAt}]}`.
|
|
1588
|
+
/// DERIVED, NOT DRAINED: a resolution write folding in removes its row by derivation.
|
|
1589
|
+
/// Empty for a Surfaced-free workspace. (The generated typed client wraps these rows as
|
|
1590
|
+
/// `PendingConflictRow` and pairs them with the designated `resolve…` helpers.)
|
|
1591
|
+
Future<List<Map<String, dynamic>>> pendingConflicts([String? session]) async =>
|
|
1592
|
+
_asRows(await _call('pendingConflicts', {
|
|
1593
|
+
if (session != null) 'session': session,
|
|
1594
|
+
}));
|
|
1595
|
+
|
|
1462
1596
|
/// Push local writes → the session branch → cloud admission, and pull canonical main back.
|
|
1463
1597
|
Future<Map<String, Object?>> sync({bool admit = true, String? session}) async =>
|
|
1464
1598
|
_asMap(await _call('sync', {
|
|
@@ -1612,6 +1746,11 @@ class NomosBridge {
|
|
|
1612
1746
|
[String? session]) =>
|
|
1613
1747
|
_watchStream(kind: 'spatial', spatialId: spatialId, bbox: bbox, session: session);
|
|
1614
1748
|
|
|
1749
|
+
/// Reactive PENDING-CONFLICTS read: re-emits the maintained Surfaced "decisions needed"
|
|
1750
|
+
/// rows whenever the index changes (derived, not drained — a resolution write clears its row).
|
|
1751
|
+
Stream<List<dynamic>> watchPendingConflicts([String? session]) =>
|
|
1752
|
+
_watchStream(kind: 'pendingConflicts', session: session);
|
|
1753
|
+
|
|
1615
1754
|
/// SHAPE COMPATIBILITY (#72): one-shot verdict for this client's structural [requirement]
|
|
1616
1755
|
/// (its baked `…InterfaceJson`, decoded) against the workspace's ON-LEDGER offer (auto-sourced).
|
|
1617
1756
|
/// `{ ok, missing[], incompatible[] }` — names the exact directive/aggregate/field, not a hash.
|
|
@@ -1808,6 +1947,13 @@ class NomosSession extends NomosBridge {
|
|
|
1808
1947
|
@override
|
|
1809
1948
|
NomosSession session(String workspace) => parent.session(workspace);
|
|
1810
1949
|
|
|
1950
|
+
/// A followed ref mounts on the SHARED plane — delegate to the parent (a session never owns mounts).
|
|
1951
|
+
@override
|
|
1952
|
+
Future<NomosSession> follow(NomosRef ref,
|
|
1953
|
+
{String? cloud, String? clientId, String? authToken, bool allowUnborn = false}) =>
|
|
1954
|
+
parent.follow(ref,
|
|
1955
|
+
cloud: cloud, clientId: clientId, authToken: authToken, allowUnborn: allowUnborn);
|
|
1956
|
+
|
|
1811
1957
|
@override
|
|
1812
1958
|
Future<Object?> _call(String op, [Map<String, Object?> args = const {}]) =>
|
|
1813
1959
|
parent._call(op,
|
package/dart/pubspec.yaml
CHANGED
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
|
|
12
12
|
code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
13
13
|
the EXACT serde JSON the Rust kernel + projection produce/consume.
|
|
14
|
-
version: 0.
|
|
14
|
+
version: 0.47.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// SESSION-BOUND CLIENTS — THE ACCEPTANCE (architecture/session_bound_clients.md). The blessed app
|
|
2
|
+
// shape end-to-end, against the REAL wasm on a local plane, with ZERO workspace/domain/directive
|
|
3
|
+
// strings in the app-side code:
|
|
4
|
+
//
|
|
5
|
+
// engine → mount → Client.bind(session) (preflight) → a PARENTLESS birth landing in the session
|
|
6
|
+
// workspace → engine.follow(ref) → a second client → a directive on it.
|
|
7
|
+
//
|
|
8
|
+
// Plus the typed failure modes: bind on a deliberately WRONG session throws
|
|
9
|
+
// DomainClientSessionMismatch with every spec field; a dispatch naming an uninstalled domain
|
|
10
|
+
// surfaces as DomainNotInstalledForSession (the context-rich replacement of the kernel's generic
|
|
11
|
+
// "no active installed law declares domain 'x'"); and the deprecated engine-level shims still
|
|
12
|
+
// function (compatibility is deprecation, not breakage).
|
|
13
|
+
@TestOn('vm')
|
|
14
|
+
library;
|
|
15
|
+
|
|
16
|
+
import 'dart:io';
|
|
17
|
+
|
|
18
|
+
// ignore: deprecated_member_use
|
|
19
|
+
import 'package:nomos_client/nomos_headless.dart';
|
|
20
|
+
import 'package:test/test.dart';
|
|
21
|
+
|
|
22
|
+
// home.client.dart re-exports home.dart + the nomos_client surface (NomosSession, the typed errors,
|
|
23
|
+
// NomosOfferOutcome, …); estate.client.dart adds the child-law client. One import each = the whole
|
|
24
|
+
// typed surface. ZERO raw strings needed below.
|
|
25
|
+
import '../../../bench/scale/fixtures/estate-birth/build/dart/src/generated/home.client.dart';
|
|
26
|
+
import '../../../bench/scale/fixtures/estate-birth/build/dart/src/generated/estate.client.dart'
|
|
27
|
+
show EstateClient, SeedEstateWorkspacePayload;
|
|
28
|
+
|
|
29
|
+
String get _root => '${Directory.current.path}/../..';
|
|
30
|
+
String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
|
|
31
|
+
String get _deployJson =>
|
|
32
|
+
'$_root/bench/scale/fixtures/estate-birth/build/estate-birth.deploy.json';
|
|
33
|
+
String get _otherDeployJson =>
|
|
34
|
+
'$_root/bench/scale/fixtures/surfaced/build/surfaced.deploy.json';
|
|
35
|
+
String get _runtimeDir =>
|
|
36
|
+
Platform.environment['NOMOS_DART_RUNTIME'] ??
|
|
37
|
+
'${Platform.environment['HOME'] ?? ''}/.holon/runtime';
|
|
38
|
+
|
|
39
|
+
const _workspace = 'estate-birth';
|
|
40
|
+
const _owner = 'user:session-bound-owner';
|
|
41
|
+
|
|
42
|
+
String? _missingPrereq() {
|
|
43
|
+
if (!File(_repoEntry).existsSync()) return 'in-repo runner not found at $_repoEntry';
|
|
44
|
+
if (!File(_deployJson).existsSync() || !File(_otherDeployJson).existsSync()) {
|
|
45
|
+
return 'compile the fixtures: (cd bench/scale/fixtures/estate-birth && node ../../../../dsl/compile_package.mjs); '
|
|
46
|
+
'(cd bench/scale/fixtures/surfaced && node ../../../../dsl/compile_package.mjs)';
|
|
47
|
+
}
|
|
48
|
+
if (!File('$_runtimeDir/holon.wasm').existsSync() ||
|
|
49
|
+
!File('$_runtimeDir/packages.json').existsSync()) {
|
|
50
|
+
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
Future<NomosTestHarness> _harness() => NomosTestHarness.ephemeral(
|
|
56
|
+
deployJsonPaths: [_deployJson],
|
|
57
|
+
nodeEntry: _repoEntry,
|
|
58
|
+
runtimeDir: _runtimeDir,
|
|
59
|
+
workspace: _workspace,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
void main() {
|
|
63
|
+
test(
|
|
64
|
+
'THE BLESSED SHAPE: mount → bind (preflight) → parentless birth in the session workspace → '
|
|
65
|
+
'follow(ref) → second client → directive — zero routing strings in app code', () async {
|
|
66
|
+
final skip = _missingPrereq();
|
|
67
|
+
if (skip != null) {
|
|
68
|
+
markTestSkipped(skip);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
final harness = await _harness();
|
|
72
|
+
try {
|
|
73
|
+
// ENGINE + SESSION: the harness bridge IS the session handle pinned to the fixture workspace.
|
|
74
|
+
final home = harness.session(harness.workspace);
|
|
75
|
+
|
|
76
|
+
// BIND (ask 2/5): the async preflight — reachable, domain installed, shape-compatible.
|
|
77
|
+
final homeClient = await HomeClient.bind(home, actor: _owner);
|
|
78
|
+
expect(homeClient.session, isNotNull);
|
|
79
|
+
expect(homeClient.session!.workspace, harness.workspace,
|
|
80
|
+
reason: 'inspectable session context (ask 3)');
|
|
81
|
+
|
|
82
|
+
// PARENTLESS BIRTH (ask 4): no parent parameter — the parent IS the session workspace.
|
|
83
|
+
final birth = await homeClient.birthEstateWorkspace(
|
|
84
|
+
payload: BirthEstateWorkspacePayload(
|
|
85
|
+
owner: _owner,
|
|
86
|
+
frameworkHash: harness.frameworkHash!,
|
|
87
|
+
lawHash: harness.lawHashes(harness.workspace).first,
|
|
88
|
+
bornAt: DateTime.now().toUtc().toIso8601String(),
|
|
89
|
+
),
|
|
90
|
+
);
|
|
91
|
+
expect(birth.workspace, harness.workspace,
|
|
92
|
+
reason: 'the birth offer landed in the SESSION workspace, no loose parent');
|
|
93
|
+
expect(birth.gatewayVerdict, isNull, reason: 'the local offer lane, not the legacy relay');
|
|
94
|
+
expect(birth.born, hasLength(1));
|
|
95
|
+
expect(birth.birthOutcomes.first['status'], 'born');
|
|
96
|
+
|
|
97
|
+
// FOLLOW THE REF (ask 8): NomosRef in, mounted session out — never a string pair.
|
|
98
|
+
final estate = await harness.bridge.follow(birth.bornRefs.single);
|
|
99
|
+
expect(estate.workspace, birth.born.single);
|
|
100
|
+
|
|
101
|
+
// SECOND CLIENT + a typed read AND a directive on it.
|
|
102
|
+
final estateClient = await EstateClient.bind(estate, actor: _owner);
|
|
103
|
+
final rows = await estateClient.estatesByOwner(owner: _owner);
|
|
104
|
+
expect(rows, hasLength(1), reason: 'the child genesis seed reads back typed');
|
|
105
|
+
final seeded = rows.single;
|
|
106
|
+
// A directive on the second client: re-ensure the singleton with the SAME seeded values
|
|
107
|
+
// (`.ensures` — idempotent; the squat invariant re-accepts its own binding).
|
|
108
|
+
final reseed = await estateClient.seedEstateWorkspace(SeedEstateWorkspacePayload(
|
|
109
|
+
owner: seeded.owner!,
|
|
110
|
+
estateName: seeded.estateName!,
|
|
111
|
+
bornAt: seeded.bornAt!,
|
|
112
|
+
));
|
|
113
|
+
expect(reseed.head, isNotEmpty);
|
|
114
|
+
|
|
115
|
+
// ask 9: the harness diagnostic names the installed domains.
|
|
116
|
+
await harness.assertDomainInstalled(
|
|
117
|
+
workspace: estate.workspace, domain: EstateClient.interface.domain);
|
|
118
|
+
} finally {
|
|
119
|
+
await harness.dispose();
|
|
120
|
+
}
|
|
121
|
+
}, timeout: const Timeout(Duration(minutes: 4)));
|
|
122
|
+
|
|
123
|
+
test('bind on a deliberately WRONG session throws DomainClientSessionMismatch with every field',
|
|
124
|
+
() async {
|
|
125
|
+
final skip = _missingPrereq();
|
|
126
|
+
if (skip != null) {
|
|
127
|
+
markTestSkipped(skip);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
// TWO workspaces, TWO different laws, ONE plane — binding HomeClient to the docs
|
|
131
|
+
// workspace is the deliberate wrong-session case (wrong-package-mounted).
|
|
132
|
+
final harness = await NomosTestHarness.cluster(
|
|
133
|
+
nodeEntry: _repoEntry,
|
|
134
|
+
runtimeDir: _runtimeDir,
|
|
135
|
+
workspaces: {
|
|
136
|
+
_workspace: [_deployJson],
|
|
137
|
+
'docs-ws': [_otherDeployJson],
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
try {
|
|
141
|
+
final wrong = harness.session('docs-ws');
|
|
142
|
+
Object? thrown;
|
|
143
|
+
try {
|
|
144
|
+
await HomeClient.bind(wrong);
|
|
145
|
+
} catch (e) {
|
|
146
|
+
thrown = e;
|
|
147
|
+
}
|
|
148
|
+
expect(thrown, isA<DomainClientSessionMismatch>(),
|
|
149
|
+
reason: 'the mismatch fires at CONSTRUCTION, not two clicks into the app');
|
|
150
|
+
final m = thrown! as DomainClientSessionMismatch;
|
|
151
|
+
expect(m.sessionId, 'docs-ws');
|
|
152
|
+
expect(m.sessionWorkspace, 'docs-ws');
|
|
153
|
+
expect(m.expectedDomainKey, HomeClient.interface.domain);
|
|
154
|
+
expect(m.expectedDomainHash, HomeClient.interface.expectedDomainHash);
|
|
155
|
+
expect(m.expectedInterfaceHash, HomeClient.interface.expectedInterfaceHash);
|
|
156
|
+
expect(m.activeDomainKeys, isNot(contains(m.expectedDomainKey)));
|
|
157
|
+
expect(m.cause, SessionMismatchCause.wrongPackageMounted,
|
|
158
|
+
reason: 'the docs workspace RUNS law — just not this domain');
|
|
159
|
+
expect(m.hint, isNotEmpty);
|
|
160
|
+
expect(m.toString(), contains(m.expectedDomainKey));
|
|
161
|
+
|
|
162
|
+
// The RIGHT session still binds (the plane is unharmed).
|
|
163
|
+
await HomeClient.bind(harness.session(harness.workspace));
|
|
164
|
+
|
|
165
|
+
// session.assertCompatible is directly usable from tests (ask 9), same typed refusal.
|
|
166
|
+
await expectLater(wrong.assertCompatible(EstateClient.interface),
|
|
167
|
+
throwsA(isA<DomainClientSessionMismatch>()));
|
|
168
|
+
} finally {
|
|
169
|
+
await harness.dispose();
|
|
170
|
+
}
|
|
171
|
+
}, timeout: const Timeout(Duration(minutes: 4)));
|
|
172
|
+
|
|
173
|
+
test(
|
|
174
|
+
'a dispatch naming an UNINSTALLED domain surfaces as DomainNotInstalledForSession '
|
|
175
|
+
'(context-rich, ask 6) — and the deprecated engine shims still function (ask 1/7)', () async {
|
|
176
|
+
final skip = _missingPrereq();
|
|
177
|
+
if (skip != null) {
|
|
178
|
+
markTestSkipped(skip);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
final harness = await _harness();
|
|
182
|
+
try {
|
|
183
|
+
final session = harness.session(harness.workspace);
|
|
184
|
+
Object? thrown;
|
|
185
|
+
try {
|
|
186
|
+
await session.offerOutcome(
|
|
187
|
+
domain: 'not_installed_domain', directiveId: 'doIt', payload: const {});
|
|
188
|
+
} catch (e) {
|
|
189
|
+
thrown = e;
|
|
190
|
+
}
|
|
191
|
+
expect(thrown, isA<DomainNotInstalledForSession>());
|
|
192
|
+
final err = thrown! as DomainNotInstalledForSession;
|
|
193
|
+
expect(err.expectedDomainKey, 'not_installed_domain');
|
|
194
|
+
expect(err.sessionWorkspace, harness.workspace);
|
|
195
|
+
expect(err.requestedWorkspace, harness.workspace);
|
|
196
|
+
expect(err.activeDomainKeys, contains(HomeClient.interface.domain),
|
|
197
|
+
reason: 'the error NAMES what the workspace actually runs');
|
|
198
|
+
expect(err.possibleCauses, DomainNotInstalledForSession.defaultPossibleCauses);
|
|
199
|
+
expect(err.underlying.toString(), contains('no active installed law declares domain'));
|
|
200
|
+
expect(err.toString(), contains('Possible causes'));
|
|
201
|
+
|
|
202
|
+
// THE DEPRECATED SHIM still functions: the engine-level generic write authors fine.
|
|
203
|
+
// ignore: deprecated_member_use
|
|
204
|
+
final head = await session.offerDirective(
|
|
205
|
+
domain: HomeClient.interface.domain,
|
|
206
|
+
directiveId: 'birthEstateWorkspace',
|
|
207
|
+
payload: {
|
|
208
|
+
'owner': 'user:legacy-lane-owner',
|
|
209
|
+
'frameworkHash': harness.frameworkHash,
|
|
210
|
+
'lawHash': harness.lawHashes(harness.workspace).first,
|
|
211
|
+
'bornAt': DateTime.now().toUtc().toIso8601String(),
|
|
212
|
+
},
|
|
213
|
+
actor: 'user:legacy-lane-owner',
|
|
214
|
+
);
|
|
215
|
+
expect(head, isNotEmpty, reason: 'deprecation, not breakage — the old lane still writes');
|
|
216
|
+
} finally {
|
|
217
|
+
await harness.dispose();
|
|
218
|
+
}
|
|
219
|
+
}, timeout: const Timeout(Duration(minutes: 4)));
|
|
220
|
+
|
|
221
|
+
test('the SYNC constructor lazily preflights on first dispatch (the can\'t-await path)',
|
|
222
|
+
() async {
|
|
223
|
+
final skip = _missingPrereq();
|
|
224
|
+
if (skip != null) {
|
|
225
|
+
markTestSkipped(skip);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
final harness = await NomosTestHarness.cluster(
|
|
229
|
+
nodeEntry: _repoEntry,
|
|
230
|
+
runtimeDir: _runtimeDir,
|
|
231
|
+
workspaces: {
|
|
232
|
+
_workspace: [_deployJson],
|
|
233
|
+
'docs-ws': [_otherDeployJson],
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
try {
|
|
237
|
+
// Constructing synchronously on the wrong session succeeds (no await possible here)…
|
|
238
|
+
final client = HomeClient(harness.session('docs-ws'), actor: _owner);
|
|
239
|
+
// …but the FIRST dispatch runs the same preflight and throws the same typed mismatch.
|
|
240
|
+
await expectLater(
|
|
241
|
+
client.birthEstateWorkspace(
|
|
242
|
+
payload: BirthEstateWorkspacePayload(
|
|
243
|
+
owner: _owner,
|
|
244
|
+
frameworkHash: harness.frameworkHash!,
|
|
245
|
+
lawHash: harness.lawHashes(harness.workspace).first,
|
|
246
|
+
bornAt: DateTime.now().toUtc().toIso8601String(),
|
|
247
|
+
),
|
|
248
|
+
),
|
|
249
|
+
throwsA(isA<DomainClientSessionMismatch>()),
|
|
250
|
+
);
|
|
251
|
+
} finally {
|
|
252
|
+
await harness.dispose();
|
|
253
|
+
}
|
|
254
|
+
}, timeout: const Timeout(Duration(minutes: 4)));
|
|
255
|
+
}
|
package/dart_types/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## 0.2.0
|
|
2
|
+
|
|
3
|
+
- `NomosPrincipal` / `NomosDeviceSecret` — branded identity types for the generated cross-workspace birth
|
|
4
|
+
methods (`.births()` directives). `actor`/`authorSecret` were previously bare, adjacent, same-shaped
|
|
5
|
+
strings — a transposition footgun with no compiler check. `NomosPrincipal.user(uid)`/`.parse('user:<uid>')`
|
|
6
|
+
and the opaque `NomosDeviceSecret` (redacts in `toString()`) make swapping them a type error. Purely
|
|
7
|
+
additive.
|
|
8
|
+
|
|
1
9
|
## 0.1.4
|
|
2
10
|
|
|
3
11
|
- Custody-lifecycle types: `NomosCustodyBreak`, `NomosCustodyStatus`, `NomosCustodyEvent`,
|
|
@@ -33,6 +33,12 @@ export 'src/errors.dart';
|
|
|
33
33
|
export 'src/foreign_ref.dart';
|
|
34
34
|
export 'src/activity.dart';
|
|
35
35
|
export 'src/nomos_ref.dart';
|
|
36
|
+
// Branded identity wrappers (NomosPrincipal / NomosDeviceSecret) for the generated cross-workspace
|
|
37
|
+
// birth methods — actor/authorSecret are distinct types, so a transposition is a compile error.
|
|
38
|
+
export 'src/principal.dart';
|
|
36
39
|
// The CUSTODY-LIFECYCLE types (rebirth_and_custody_lifecycle.md): NomosCustodyBreak / NomosCustodyStatus /
|
|
37
40
|
// CustodyBreakPolicy — custody incompatibility as a first-class, typed, app-decidable event. Pure data.
|
|
38
41
|
export 'src/custody.dart';
|
|
42
|
+
// SURFACED MERGE CONFLICTS (architecture/surfaced_merge_conflict.md): the read-collapse types for a
|
|
43
|
+
// `.merge(Surfaced(...))` field — SurfacedValue<T> (resolved scalar | pending observations). Pure data.
|
|
44
|
+
export 'src/surfaced.dart';
|