@githolon/dsl 0.105.4 → 0.105.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 +38 -0
- package/dart/lib/src/runner/node-entry.mjs +223 -33
- package/dart/lib/src/runner/offline-install.mjs +263 -37
- package/dart/lib/src/webview_bridge.dart +149 -16
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/acquire_session_test.dart +37 -0
- package/dart/test/residency_refusal_test.dart +49 -0
- package/dart/test/sealing_client_test.dart +1 -1
- package/dart/test/webview_bridge_test.dart +19 -0
- package/dart_types/CHANGELOG.md +13 -0
- package/dart_types/lib/nomos_types.dart +4 -0
- package/dart_types/lib/src/local_work_rejected.dart +117 -0
- package/dart_types/lib/src/subscriptions.dart +5 -1
- package/dart_types/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/runtime/generated-application-host.js +202 -32
- package/src/check_package_main.ts +25 -54
- package/src/codegen_ts.ts +7 -0
- package/src/compat_package_main.ts +36 -30
- package/src/compile_package_main.ts +44 -39
- package/src/decl_shapes.ts +1 -1
- package/src/engine_entry.ts +2 -2
- package/src/evolve_preview.ts +7 -7
- package/src/identity_model.ts +123 -0
- package/src/identity_usda.ts +548 -0
- package/src/law_identity.ts +15 -0
- package/src/lsp_server.ts +14 -37
- package/src/stable_ids.ts +1 -1
- package/src/stable_id_lockfile.ts +0 -278
|
@@ -34,7 +34,8 @@ import 'package:nomos_types/nomos_types.dart'
|
|
|
34
34
|
ReferenceVersionUnavailable,
|
|
35
35
|
CustodyBreakPolicy,
|
|
36
36
|
NomosCustodyEvent,
|
|
37
|
-
NomosCustodyStatus
|
|
37
|
+
NomosCustodyStatus,
|
|
38
|
+
NomosLocalWorkRejectedEvent;
|
|
38
39
|
import 'package:nomos_types/src/nomos_ref.dart' show NmsInternalNomosRefs;
|
|
39
40
|
import 'package:nomos_types/src/versioned_nomos_ref.dart'
|
|
40
41
|
show NmsInternalVersionedNomosRefs;
|
|
@@ -1230,6 +1231,11 @@ class NomosBridge {
|
|
|
1230
1231
|
// [resolveCustodyBreak].
|
|
1231
1232
|
final StreamController<NomosCustodyEvent> _custodyEvents =
|
|
1232
1233
|
StreamController<NomosCustodyEvent>.broadcast();
|
|
1234
|
+
// LOCAL-WORK-REJECTED LIFECYCLE (seated_barrier_and_seal_obligation.md Part 4): the typed "the
|
|
1235
|
+
// cloud refused work this device had accepted" event — refused intents already parked (client DLQ
|
|
1236
|
+
// + refused/* custody refs), survivors already replayed; the app renders honest UX from this.
|
|
1237
|
+
final StreamController<NomosLocalWorkRejectedEvent> _localWorkRejectedEvents =
|
|
1238
|
+
StreamController<NomosLocalWorkRejectedEvent>.broadcast();
|
|
1233
1239
|
final StreamController<Map<String, Object?>> _flameEvents =
|
|
1234
1240
|
StreamController<Map<String, Object?>>.broadcast();
|
|
1235
1241
|
final StreamController<Map<String, Object?>> _spanEvents =
|
|
@@ -1505,6 +1511,17 @@ class NomosBridge {
|
|
|
1505
1511
|
if (ev != null && !_custodyEvents.isClosed) _custodyEvents.add(ev);
|
|
1506
1512
|
return;
|
|
1507
1513
|
}
|
|
1514
|
+
if (event == 'localWorkRejected') {
|
|
1515
|
+
// LOCAL-WORK-REJECTED: { event:'localWorkRejected', session, localWorkRejected:{ workspace,
|
|
1516
|
+
// refused, parked, resetTo, retryable, source } }. The framework already parked the refused
|
|
1517
|
+
// intents + replayed survivors; the app's only job is the honest message (retryable ⇒ "queued,
|
|
1518
|
+
// will retry"; terminal ⇒ "preserved for review — contact support").
|
|
1519
|
+
final ev = NomosLocalWorkRejectedEvent.fromMessage(m);
|
|
1520
|
+
if (ev != null && !_localWorkRejectedEvents.isClosed) {
|
|
1521
|
+
_localWorkRejectedEvents.add(ev);
|
|
1522
|
+
}
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1508
1525
|
if (event == 'transfer') {
|
|
1509
1526
|
// LIVE pack byte-progress { received, total, kind } — the real loading bar (host Content-Length).
|
|
1510
1527
|
final tp = TransferProgress.fromMessage(m);
|
|
@@ -1585,13 +1602,27 @@ class NomosBridge {
|
|
|
1585
1602
|
sampled: true,
|
|
1586
1603
|
);
|
|
1587
1604
|
_pendingTraces[id] = trace;
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1605
|
+
try {
|
|
1606
|
+
_transport.send({
|
|
1607
|
+
'v': kWebviewBridgeVersion,
|
|
1608
|
+
'id': id,
|
|
1609
|
+
'op': op,
|
|
1610
|
+
'args': args,
|
|
1611
|
+
'trace': trace.toJson(),
|
|
1612
|
+
});
|
|
1613
|
+
} catch (error, stackTrace) {
|
|
1614
|
+
// A transport that refuses (disposed at teardown, closed pipe) must fail
|
|
1615
|
+
// THE FUTURE, not throw synchronously: a sync throw bypasses every
|
|
1616
|
+
// `.catchError`/`onError` guard callers attach for exactly this case
|
|
1617
|
+
// (watch re-registration and unwatch during dispose), leaks the pending
|
|
1618
|
+
// completer, and lands as an unhandled error in whatever zone happened to
|
|
1619
|
+
// be current — the "thrown after the test had completed" noise that buried
|
|
1620
|
+
// ~100 real test results. Same loud error, delivered where errors go.
|
|
1621
|
+
_pending.remove(id);
|
|
1622
|
+
_pendingOps.remove(id);
|
|
1623
|
+
_pendingTraces.remove(id);
|
|
1624
|
+
completer.completeError(error, stackTrace);
|
|
1625
|
+
}
|
|
1595
1626
|
return completer.future;
|
|
1596
1627
|
}
|
|
1597
1628
|
|
|
@@ -1774,6 +1805,23 @@ class NomosBridge {
|
|
|
1774
1805
|
}).toList();
|
|
1775
1806
|
}
|
|
1776
1807
|
|
|
1808
|
+
/// Framework-private plane persistence primitive. It snapshots exact local custody even when [workspace]
|
|
1809
|
+
/// was born in the kernel but no Dart runner session has ever acquired it. This does not connect, mount a
|
|
1810
|
+
/// tenant session, fetch from cloud, or grant authority; the serving realm exports its resident plane view.
|
|
1811
|
+
Future<({String workspace, String head, String? snapshot, bool unchanged})>
|
|
1812
|
+
nomosInternalSnapshotWorkspace(String workspace, {String? ifHead}) async {
|
|
1813
|
+
final r = _asMap(await _call('snapshotWorkspace', {
|
|
1814
|
+
'workspace': workspace,
|
|
1815
|
+
if (ifHead != null && ifHead.isNotEmpty) 'ifHead': ifHead,
|
|
1816
|
+
}));
|
|
1817
|
+
return (
|
|
1818
|
+
workspace: (r['workspace'] ?? workspace).toString(),
|
|
1819
|
+
head: (r['head'] ?? '').toString(),
|
|
1820
|
+
snapshot: r['snapshot']?.toString(),
|
|
1821
|
+
unchanged: r['unchanged'] == true,
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1777
1825
|
/// Open a workspace on this plane: boot its LOCAL githolon under the installed law (from [restoreFrom]
|
|
1778
1826
|
/// custody, or a cloud pull when [cloud] is given). This is the sugar the single-workspace path uses; a
|
|
1779
1827
|
/// multi-workspace app names a [session] per workspace and holds their [session] handles.
|
|
@@ -1850,6 +1898,15 @@ class NomosBridge {
|
|
|
1850
1898
|
Stream<NomosCustodyEvent> get custodyEvents =>
|
|
1851
1899
|
_eventPlane._custodyEvents.stream;
|
|
1852
1900
|
|
|
1901
|
+
/// THE LOCAL-WORK-REJECTED STREAM (seated_barrier_and_seal_obligation.md Part 4): the cloud's gate
|
|
1902
|
+
/// refused work this device's own kernel had accepted. Refused intents are already durably parked
|
|
1903
|
+
/// (client DLQ + the workspace's refused/* refs) and surviving local work already replayed when an
|
|
1904
|
+
/// event arrives — bind honest UX to it: `retryable` ⇒ "your changes are queued and will retry";
|
|
1905
|
+
/// otherwise ⇒ "your changes were preserved for review — contact support". Never render the raw
|
|
1906
|
+
/// refusal text as the headline.
|
|
1907
|
+
Stream<NomosLocalWorkRejectedEvent> get localWorkRejectedEvents =>
|
|
1908
|
+
_eventPlane._localWorkRejectedEvents.stream;
|
|
1909
|
+
|
|
1853
1910
|
/// Where a session sits on the custody spectrum (incarnation, posture, pending/salvaged local work,
|
|
1854
1911
|
/// supersession history) — for a status banner or a "you're offline on old truth" indicator.
|
|
1855
1912
|
Future<NomosCustodyStatus> custodyStatus({String? session}) async =>
|
|
@@ -2325,7 +2382,52 @@ class NomosBridge {
|
|
|
2325
2382
|
}
|
|
2326
2383
|
// SINGLE-FLIGHT: one in-flight open per workspace per plane; concurrent callers join it.
|
|
2327
2384
|
return plane._acquiring.putIfAbsent(workspace, () async {
|
|
2328
|
-
|
|
2385
|
+
// RESIDENCY IS THE KERNEL'S TO DEMAND AND OURS TO SATISFY. Replaying a chain can reach a
|
|
2386
|
+
// colocated read whose source workspace is not mounted; the kernel refuses TYPED, naming it.
|
|
2387
|
+
// Mount the named sibling — which consults local custody first and only then clones — and
|
|
2388
|
+
// retry. Bounded, and each name is satisfied at most once, so a genuinely missing workspace
|
|
2389
|
+
// fails with the kernel's own refusal instead of spinning.
|
|
2390
|
+
final satisfied = <String>{};
|
|
2391
|
+
for (var round = 0;; round++) {
|
|
2392
|
+
try {
|
|
2393
|
+
return await _acquireSessionOnce(
|
|
2394
|
+
workspace,
|
|
2395
|
+
ready: ready,
|
|
2396
|
+
inheritPrincipal: inheritPrincipal,
|
|
2397
|
+
ifAbsentCredentials: ifAbsentCredentials,
|
|
2398
|
+
authorSecret: authorSecret,
|
|
2399
|
+
);
|
|
2400
|
+
} catch (error) {
|
|
2401
|
+
final required = nomosInternalResidencyRequiredWorkspace(error);
|
|
2402
|
+
if (required == null ||
|
|
2403
|
+
required == workspace ||
|
|
2404
|
+
!satisfied.add(required) ||
|
|
2405
|
+
round >= 8) {
|
|
2406
|
+
rethrow;
|
|
2407
|
+
}
|
|
2408
|
+
await acquireSession(
|
|
2409
|
+
required,
|
|
2410
|
+
ifAbsentCredentials: ifAbsentCredentials,
|
|
2411
|
+
authorSecret: authorSecret,
|
|
2412
|
+
inheritedPrincipal: sourcePrincipal,
|
|
2413
|
+
);
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
}).whenComplete(() {
|
|
2417
|
+
plane._acquiring.remove(workspace);
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
Future<NomosSession> _acquireSessionOnce(
|
|
2422
|
+
String workspace, {
|
|
2423
|
+
required Future<NomosSession> Function(NomosSession) ready,
|
|
2424
|
+
required void Function() inheritPrincipal,
|
|
2425
|
+
Future<NomosSessionCredentials> Function()? ifAbsentCredentials,
|
|
2426
|
+
String? authorSecret,
|
|
2427
|
+
}) async {
|
|
2428
|
+
final plane = _eventPlane;
|
|
2429
|
+
{
|
|
2430
|
+
{
|
|
2329
2431
|
if (plane._sessionNames.contains(workspace)) {
|
|
2330
2432
|
return ready(session(workspace));
|
|
2331
2433
|
}
|
|
@@ -2401,11 +2503,8 @@ class NomosBridge {
|
|
|
2401
2503
|
allowUnborn: c.allowUnborn,
|
|
2402
2504
|
);
|
|
2403
2505
|
return ready(session(workspace));
|
|
2404
|
-
} finally {
|
|
2405
|
-
// completed (either way) — the next acquire re-evaluates fresh (a live session hits the fast path).
|
|
2406
|
-
plane._acquiring.remove(workspace);
|
|
2407
2506
|
}
|
|
2408
|
-
}
|
|
2507
|
+
}
|
|
2409
2508
|
}
|
|
2410
2509
|
|
|
2411
2510
|
/// @internal Attach the generated application's opaque local-custody store
|
|
@@ -2805,18 +2904,23 @@ class NomosBridge {
|
|
|
2805
2904
|
/// `principal` on `parent`'s chain (`identity:<principal>#signer@key:<sha256(publicKey)>`)? One O(1)
|
|
2806
2905
|
/// relation read over the open gateway; nothing is authored. Returns
|
|
2807
2906
|
/// `{parent, principal, keyHash, object, relation, enrolled, revoked, checkedAt}`.
|
|
2907
|
+
/// Pass `remote: true` to force the CLOUD-gateway read even when this holon IS the parent — the
|
|
2908
|
+
/// seated barrier's proof-by-read: `enrolled: true` then means the shared chain durably holds this
|
|
2909
|
+
/// device's signer tuple, not merely that the local fold ran.
|
|
2808
2910
|
Future<Map<String, Object?>> parentSignerEnrollment(
|
|
2809
2911
|
{required String parent,
|
|
2810
2912
|
required String principal,
|
|
2811
2913
|
String? keyHash,
|
|
2812
2914
|
String? publicKey,
|
|
2813
|
-
String? authToken
|
|
2915
|
+
String? authToken,
|
|
2916
|
+
bool remote = false}) async =>
|
|
2814
2917
|
_asMap(await _call('parentSignerEnrollment', {
|
|
2815
2918
|
'parent': parent,
|
|
2816
2919
|
'principal': principal,
|
|
2817
2920
|
if (keyHash != null) 'keyHash': keyHash,
|
|
2818
2921
|
if (publicKey != null) 'publicKey': publicKey,
|
|
2819
2922
|
if (authToken != null) 'authToken': authToken,
|
|
2923
|
+
if (remote) 'remote': true,
|
|
2820
2924
|
}));
|
|
2821
2925
|
|
|
2822
2926
|
/// IDEMPOTENT PARENT-SIGNER RECOVERY — ensure this device key is a live `signer` on `parent`,
|
|
@@ -3623,24 +3727,34 @@ class NomosBridge {
|
|
|
3623
3727
|
required int t0,
|
|
3624
3728
|
required int t1,
|
|
3625
3729
|
String? id,
|
|
3730
|
+
String? session,
|
|
3731
|
+
String? head,
|
|
3626
3732
|
}) {
|
|
3627
3733
|
final plane = _eventPlane;
|
|
3628
3734
|
final sid = id ?? 'local-${plane._localSpanSeq++}';
|
|
3629
3735
|
for (final m in <Map<String, Object?>>[
|
|
3630
3736
|
{
|
|
3631
3737
|
'event': 'span',
|
|
3738
|
+
if (session != null && session.isNotEmpty) 'session': session,
|
|
3632
3739
|
'span': {
|
|
3633
3740
|
'phase': 'begin',
|
|
3634
3741
|
'id': sid,
|
|
3635
3742
|
'track': track,
|
|
3636
3743
|
'name': name,
|
|
3637
3744
|
'kind': kind,
|
|
3638
|
-
't0': t0
|
|
3745
|
+
't0': t0,
|
|
3746
|
+
if (head != null && head.isNotEmpty) 'head': head,
|
|
3639
3747
|
}
|
|
3640
3748
|
},
|
|
3641
3749
|
{
|
|
3642
3750
|
'event': 'span',
|
|
3643
|
-
|
|
3751
|
+
if (session != null && session.isNotEmpty) 'session': session,
|
|
3752
|
+
'span': {
|
|
3753
|
+
'phase': 'end',
|
|
3754
|
+
'id': sid,
|
|
3755
|
+
't1': t1,
|
|
3756
|
+
if (head != null && head.isNotEmpty) 'head': head,
|
|
3757
|
+
}
|
|
3644
3758
|
},
|
|
3645
3759
|
]) {
|
|
3646
3760
|
plane._spanBuffer.add(m);
|
|
@@ -3927,6 +4041,7 @@ class NomosBridge {
|
|
|
3927
4041
|
if (!_syncEvents.isClosed) await _syncEvents.close();
|
|
3928
4042
|
if (!_signalEvents.isClosed) await _signalEvents.close();
|
|
3929
4043
|
if (!_custodyEvents.isClosed) await _custodyEvents.close();
|
|
4044
|
+
if (!_localWorkRejectedEvents.isClosed) await _localWorkRejectedEvents.close();
|
|
3930
4045
|
if (!_flameEvents.isClosed) await _flameEvents.close();
|
|
3931
4046
|
if (!_spanEvents.isClosed) await _spanEvents.close();
|
|
3932
4047
|
if (!_transferEvents.isClosed) await _transferEvents.close();
|
|
@@ -4238,3 +4353,21 @@ class _BridgeForeignWorkspaceReader implements NomosForeignWorkspaceReader {
|
|
|
4238
4353
|
return ctrl.stream;
|
|
4239
4354
|
}
|
|
4240
4355
|
}
|
|
4356
|
+
|
|
4357
|
+
/// THE KERNEL NAMES WHAT IT NEEDS. When a chain replay reaches a colocated foreign read whose
|
|
4358
|
+
/// source workspace is not mounted, the kernel refuses with a typed, machine-readable message
|
|
4359
|
+
/// naming that workspace. The cloud has satisfied this since the requirement was introduced; the
|
|
4360
|
+
/// client never learned to, so an estate whose history reads a catalogue simply could not open on a
|
|
4361
|
+
/// device — the same kernel, the same rule, only one implementer.
|
|
4362
|
+
///
|
|
4363
|
+
/// Parses the workspace name out of the refusal, or null when this is some other failure. Matching
|
|
4364
|
+
/// on the message is deliberate and narrow: the refusal is kernel-authored and stable, and the
|
|
4365
|
+
/// alternative — treating every open failure as a residency miss — would retry real errors forever.
|
|
4366
|
+
String? nomosInternalResidencyRequiredWorkspace(Object error) {
|
|
4367
|
+
final text = error.toString();
|
|
4368
|
+
if (!text.contains('foreign-read-not-ready')) return null;
|
|
4369
|
+
final match = RegExp(r"source workspace '([^']+)' is not resident")
|
|
4370
|
+
.firstMatch(text);
|
|
4371
|
+
final workspace = match?.group(1)?.trim();
|
|
4372
|
+
return (workspace == null || workspace.isEmpty) ? null : workspace;
|
|
4373
|
+
}
|
package/dart/pubspec.lock
CHANGED
package/dart/pubspec.yaml
CHANGED
|
@@ -7,7 +7,7 @@ repository: https://github.com/Captain-App/nomos2
|
|
|
7
7
|
description: >-
|
|
8
8
|
Nomos 2 Dart client — generated domain values, intent payloads and application
|
|
9
9
|
lifecycle types over the typed Nomos kernel protocol and USDA custody model.
|
|
10
|
-
version: 0.64.
|
|
10
|
+
version: 0.64.23
|
|
11
11
|
|
|
12
12
|
environment:
|
|
13
13
|
sdk: ^3.5.0
|
|
@@ -16,7 +16,7 @@ dependencies:
|
|
|
16
16
|
# The pure-data core (wire types, offer codec, NomosReads, schema validation, error
|
|
17
17
|
# info). nomos_client depends on and re-exports it, so existing imports are unchanged.
|
|
18
18
|
# For local monorepo dev, pubspec_overrides.yaml points this at ../dart_types.
|
|
19
|
-
nomos_types: ^0.8.
|
|
19
|
+
nomos_types: ^0.8.7
|
|
20
20
|
json_schema: ^5.2.2
|
|
21
21
|
crypto: ^3.0.0
|
|
22
22
|
package_config: ^2.1.0
|
|
@@ -70,6 +70,17 @@ class FakeRunner implements BridgeTransport {
|
|
|
70
70
|
]
|
|
71
71
|
};
|
|
72
72
|
break;
|
|
73
|
+
case 'snapshotWorkspace':
|
|
74
|
+
result = {
|
|
75
|
+
'workspace': args['workspace'],
|
|
76
|
+
'head': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
77
|
+
if (args['ifHead'] ==
|
|
78
|
+
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
|
|
79
|
+
'unchanged': true
|
|
80
|
+
else
|
|
81
|
+
'snapshot': 'resident-snapshot-b64',
|
|
82
|
+
};
|
|
83
|
+
break;
|
|
73
84
|
default:
|
|
74
85
|
result = {};
|
|
75
86
|
}
|
|
@@ -392,6 +403,32 @@ void main() {
|
|
|
392
403
|
await bridge.dispose();
|
|
393
404
|
});
|
|
394
405
|
|
|
406
|
+
test(
|
|
407
|
+
'plane snapshot exports resident custody without manufacturing a Dart session',
|
|
408
|
+
() async {
|
|
409
|
+
final runner = FakeRunner(resident: ['born-child']);
|
|
410
|
+
final bridge = NomosBridge(runner);
|
|
411
|
+
await bridge.ready;
|
|
412
|
+
|
|
413
|
+
final saved =
|
|
414
|
+
await bridge.nomosInternalSnapshotWorkspace('born-child');
|
|
415
|
+
expect(saved.workspace, 'born-child');
|
|
416
|
+
expect(saved.head, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa');
|
|
417
|
+
expect(saved.snapshot, 'resident-snapshot-b64');
|
|
418
|
+
expect(saved.unchanged, isFalse);
|
|
419
|
+
expect(runner.connectCount, 0,
|
|
420
|
+
reason:
|
|
421
|
+
'save-all must not acquire an unopened child or invent host authority');
|
|
422
|
+
|
|
423
|
+
final unchanged = await bridge.nomosInternalSnapshotWorkspace(
|
|
424
|
+
'born-child',
|
|
425
|
+
ifHead: saved.head);
|
|
426
|
+
expect(unchanged.unchanged, isTrue);
|
|
427
|
+
expect(unchanged.snapshot, isNull);
|
|
428
|
+
expect(runner.connectCount, 0);
|
|
429
|
+
await bridge.dispose();
|
|
430
|
+
});
|
|
431
|
+
|
|
395
432
|
test(
|
|
396
433
|
'NomosSession.isConnected + assertConnected: fail at BIND, not on first query',
|
|
397
434
|
() async {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// THE KERNEL NAMES WHAT IT NEEDS; EVERY HOST MUST SATISFY IT.
|
|
2
|
+
//
|
|
3
|
+
// The colocated-read residency rule is enforced by the KERNEL, so it fires identically in the cloud
|
|
4
|
+
// container and on a phone — the whole point of one artifact everywhere. It shipped with only the
|
|
5
|
+
// cloud taught to satisfy it, so an estate whose history reads a catalogue could not open on a
|
|
6
|
+
// device at all. These tests pin the client's half: the exact refusal the kernel emits must be
|
|
7
|
+
// recognised, and nothing else must be mistaken for it.
|
|
8
|
+
import 'package:test/test.dart';
|
|
9
|
+
import 'package:nomos_client/src/webview_bridge.dart'
|
|
10
|
+
show nomosInternalResidencyRequiredWorkspace;
|
|
11
|
+
|
|
12
|
+
void main() {
|
|
13
|
+
test('recognises the kernel refusal and names the workspace to mount', () {
|
|
14
|
+
// Verbatim from a device, 2026-08-07 (build 1182, opening an estate).
|
|
15
|
+
const refusal =
|
|
16
|
+
'NomosBridgeError: NMS-KERNEL-CALL: restore_main: imported custody was refused: '
|
|
17
|
+
'{"atCommit":"2059537e7c7d1dcaa9b4f6f4ac2ded2ae59b53a5","atIndex":20,'
|
|
18
|
+
'"check":"colocated-read","error":"foreign-read-not-ready: source workspace '
|
|
19
|
+
"'catalogue-531e3764f974ff741be62b119a28f0a9dbf396d7' is not resident in this kernel\","
|
|
20
|
+
'"mode":"verify_chain","ok":true,"valid":false,"walk":"full"}';
|
|
21
|
+
expect(
|
|
22
|
+
nomosInternalResidencyRequiredWorkspace(refusal),
|
|
23
|
+
'catalogue-531e3764f974ff741be62b119a28f0a9dbf396d7',
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('does not mistake other failures for a residency miss', () {
|
|
28
|
+
// If any of these were read as "mount something and retry", a real error would retry forever.
|
|
29
|
+
for (final other in <String>[
|
|
30
|
+
'NomosBridgeError: NMS-KERNEL-CALL: restore_main: verification failed: bad signature',
|
|
31
|
+
'NomosSeatingPendingFailure: signer enrollment is not yet provably durable',
|
|
32
|
+
'StateError: the workspace is not on this device\'s plane',
|
|
33
|
+
'',
|
|
34
|
+
]) {
|
|
35
|
+
expect(nomosInternalResidencyRequiredWorkspace(other), isNull,
|
|
36
|
+
reason: 'must not treat "$other" as a residency requirement');
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('the sibling refusal that names no workspace is not actionable', () {
|
|
41
|
+
// The kernel has a second foreign-read-not-ready shape for an unsafe path. There is nothing to
|
|
42
|
+
// mount, so it must fall through to the caller rather than being retried.
|
|
43
|
+
expect(
|
|
44
|
+
nomosInternalResidencyRequiredWorkspace(
|
|
45
|
+
"foreign-read-not-ready: source workspace '../escape' is not a safe internal custody path"),
|
|
46
|
+
isNull,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -19,7 +19,7 @@ String get _root => '${Directory.current.path}/../..';
|
|
|
19
19
|
String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
|
|
20
20
|
String get _fixture => '$_root/cloud/web-client/test/fixtures/encrypted-notes';
|
|
21
21
|
String get _deploy => '$_fixture/build/encrypted-notes.law.usdz';
|
|
22
|
-
String get _lockfile => '$_fixture/nomos.
|
|
22
|
+
String get _lockfile => '$_fixture/nomos.identity.usda';
|
|
23
23
|
String get _runtimeDir =>
|
|
24
24
|
Platform.environment['NOMOS_DART_RUNTIME'] ??
|
|
25
25
|
Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
|
|
@@ -618,6 +618,25 @@ void main() {
|
|
|
618
618
|
await bridge.ready.timeout(const Duration(seconds: 1));
|
|
619
619
|
});
|
|
620
620
|
|
|
621
|
+
test('a local durability span remains workspace/head specific', () async {
|
|
622
|
+
await bridge.ready;
|
|
623
|
+
bridge.noteLocalSpan(
|
|
624
|
+
track: 'submit',
|
|
625
|
+
name: 'durable save (disk)',
|
|
626
|
+
kind: 'persist',
|
|
627
|
+
t0: 100,
|
|
628
|
+
t1: 120,
|
|
629
|
+
session: 'born-child',
|
|
630
|
+
head: '2222222222222222222222222222222222222222',
|
|
631
|
+
);
|
|
632
|
+
final saved = bridge.recentSpans
|
|
633
|
+
.where((event) => event['session'] == 'born-child')
|
|
634
|
+
.toList();
|
|
635
|
+
expect(saved, hasLength(2));
|
|
636
|
+
expect((saved.last['span'] as Map)['head'],
|
|
637
|
+
'2222222222222222222222222222222222222222');
|
|
638
|
+
});
|
|
639
|
+
|
|
621
640
|
test('dispose closes active watches without mutating its iteration',
|
|
622
641
|
() async {
|
|
623
642
|
await bridge.ready;
|
package/dart_types/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## 0.8.7
|
|
2
|
+
|
|
3
|
+
- Decode only the kernel's stamped structural map-entry envelope as an envelope. Ordinary domain
|
|
4
|
+
objects that happen to contain a `value` field now retain all of their fields.
|
|
5
|
+
|
|
6
|
+
## 0.8.6
|
|
7
|
+
|
|
8
|
+
- NEW: `NomosLocalWorkRejectedEvent` / `NomosRejectedIntent` — the typed "the cloud refused work your
|
|
9
|
+
device had accepted" lifecycle event (architecture/seated_barrier_and_seal_obligation.md Part 4).
|
|
10
|
+
By the time it fires, refused intents are durably parked on BOTH sides (client DLQ + workspace
|
|
11
|
+
refused/* refs) and surviving local work is rebase-replayed — no work is ever lost. `retryable`
|
|
12
|
+
distinguishes transient-premise refusals (auto-retry clears them) from terminal ones.
|
|
13
|
+
|
|
1
14
|
## 0.8.5
|
|
2
15
|
|
|
3
16
|
- Expose bounded generated-startup and referenced-workspace custody failures as typed data so applications
|
|
@@ -50,6 +50,10 @@ export 'src/git_object_id.dart';
|
|
|
50
50
|
// The CUSTODY-LIFECYCLE types (rebirth_and_custody_lifecycle.md): NomosCustodyBreak / NomosCustodyStatus /
|
|
51
51
|
// CustodyBreakPolicy — custody incompatibility as a first-class, typed, app-decidable event. Pure data.
|
|
52
52
|
export 'src/custody.dart';
|
|
53
|
+
// THE LOCAL-WORK-REJECTED LIFECYCLE (seated_barrier_and_seal_obligation.md Part 4):
|
|
54
|
+
// NomosLocalWorkRejectedEvent / NomosRejectedIntent — "the cloud refused work your device had
|
|
55
|
+
// accepted" as a first-class, typed, app-renderable event (refused work is parked, never lost).
|
|
56
|
+
export 'src/local_work_rejected.dart';
|
|
53
57
|
// SURFACED MERGE CONFLICTS (architecture/surfaced_merge_conflict.md): the read-collapse types for a
|
|
54
58
|
// `.merge(Surfaced(...))` field — SurfacedValue<T> (resolved scalar | pending observations). Pure data.
|
|
55
59
|
export 'src/surfaced.dart';
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// THE LOCAL-WORK-REJECTED LIFECYCLE EVENT (architecture/seated_barrier_and_seal_obligation.md Part 4).
|
|
2
|
+
//
|
|
3
|
+
// Fired when work THIS device authored locally — and which its OWN kernel admitted — is REFUSED by the
|
|
4
|
+
// shared workspace's gate on sync/admission. Local-first means local acceptance is a real, honest
|
|
5
|
+
// verdict; the shared chain's gate re-judges from ITS state, and the two can disagree (a premise the
|
|
6
|
+
// device relied on hadn't landed yet, or the write genuinely violates shared law). By the time this
|
|
7
|
+
// event fires the framework has already done the right thing:
|
|
8
|
+
//
|
|
9
|
+
// • every refused intent is durably PARKED on BOTH sides (the client dead-letter queue AND the
|
|
10
|
+
// workspace's refused/* custody refs) — inspectable, re-offerable, NEVER lost;
|
|
11
|
+
// • every surviving unrefused local intent was rebase-replayed onto canonical main;
|
|
12
|
+
// • local state converged to [resetTo].
|
|
13
|
+
//
|
|
14
|
+
// The app's ONLY job is honest UX. [retryable] is the fork in the message:
|
|
15
|
+
// true ⇒ "Your changes are queued and will be retried automatically." (transient premise — e.g.
|
|
16
|
+
// this device's signer enrollment hadn't landed on the chain yet)
|
|
17
|
+
// false ⇒ "Your changes couldn't be accepted and have been preserved for review — contact support."
|
|
18
|
+
// Never show [NomosRejectedIntent.error] raw to an end user — it is the gate's diagnostic text.
|
|
19
|
+
class NomosRejectedIntent {
|
|
20
|
+
final String? intentId;
|
|
21
|
+
final String? oid;
|
|
22
|
+
final String? domain;
|
|
23
|
+
final String? directiveId;
|
|
24
|
+
|
|
25
|
+
/// The gate's own refusal text (diagnostic — for logs/support disclosure, never the headline).
|
|
26
|
+
final String error;
|
|
27
|
+
|
|
28
|
+
/// Transient-premise refusal that auto-retry can clear.
|
|
29
|
+
final bool retryable;
|
|
30
|
+
|
|
31
|
+
const NomosRejectedIntent({
|
|
32
|
+
this.intentId,
|
|
33
|
+
this.oid,
|
|
34
|
+
this.domain,
|
|
35
|
+
this.directiveId,
|
|
36
|
+
required this.error,
|
|
37
|
+
required this.retryable,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
factory NomosRejectedIntent.fromJson(Map<String, dynamic> j) =>
|
|
41
|
+
NomosRejectedIntent(
|
|
42
|
+
intentId: j['intentId'] as String?,
|
|
43
|
+
oid: j['oid'] as String?,
|
|
44
|
+
domain: j['domain'] as String?,
|
|
45
|
+
directiveId: j['directiveId'] as String?,
|
|
46
|
+
error: (j['error'] ?? '') as String,
|
|
47
|
+
retryable: j['retryable'] == true,
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
Map<String, dynamic> toJson() => {
|
|
51
|
+
'intentId': intentId,
|
|
52
|
+
'oid': oid,
|
|
53
|
+
'domain': domain,
|
|
54
|
+
'directiveId': directiveId,
|
|
55
|
+
'error': error,
|
|
56
|
+
'retryable': retryable,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// The typed "the cloud refused work your device had accepted" event. See library docs above —
|
|
61
|
+
/// refused work is already parked and survivors replayed when this fires; render honest UX only.
|
|
62
|
+
class NomosLocalWorkRejectedEvent {
|
|
63
|
+
final String workspace;
|
|
64
|
+
final String? session;
|
|
65
|
+
final List<NomosRejectedIntent> refused;
|
|
66
|
+
|
|
67
|
+
/// Always true when the event fires: the refused intents are durably parked (client DLQ +
|
|
68
|
+
/// workspace refused/* refs).
|
|
69
|
+
final bool parked;
|
|
70
|
+
|
|
71
|
+
/// The canonical main local state converged to.
|
|
72
|
+
final String? resetTo;
|
|
73
|
+
|
|
74
|
+
/// True iff EVERY refusal in the batch is retryable.
|
|
75
|
+
final bool retryable;
|
|
76
|
+
|
|
77
|
+
/// "admission" (the edge refused our offers) or "sync" (the offer-gated converge dead-lettered
|
|
78
|
+
/// local tail intents).
|
|
79
|
+
final String source;
|
|
80
|
+
|
|
81
|
+
const NomosLocalWorkRejectedEvent({
|
|
82
|
+
required this.workspace,
|
|
83
|
+
this.session,
|
|
84
|
+
required this.refused,
|
|
85
|
+
required this.parked,
|
|
86
|
+
this.resetTo,
|
|
87
|
+
required this.retryable,
|
|
88
|
+
required this.source,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
static NomosLocalWorkRejectedEvent? fromMessage(Map<String, dynamic> m) {
|
|
92
|
+
final e = (m['localWorkRejected'] as Map?)?.cast<String, dynamic>();
|
|
93
|
+
if (e == null) return null;
|
|
94
|
+
return NomosLocalWorkRejectedEvent(
|
|
95
|
+
workspace: (e['workspace'] ?? '') as String,
|
|
96
|
+
session: m['session'] as String?,
|
|
97
|
+
refused: ((e['refused'] as List?) ?? const [])
|
|
98
|
+
.whereType<Map>()
|
|
99
|
+
.map((r) => NomosRejectedIntent.fromJson(r.cast<String, dynamic>()))
|
|
100
|
+
.toList(growable: false),
|
|
101
|
+
parked: e['parked'] != false,
|
|
102
|
+
resetTo: e['resetTo'] as String?,
|
|
103
|
+
retryable: e['retryable'] == true,
|
|
104
|
+
source: (e['source'] ?? 'sync') as String,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
Map<String, dynamic> toJson() => {
|
|
109
|
+
'workspace': workspace,
|
|
110
|
+
'session': session,
|
|
111
|
+
'refused': refused.map((r) => r.toJson()).toList(growable: false),
|
|
112
|
+
'parked': parked,
|
|
113
|
+
'resetTo': resetTo,
|
|
114
|
+
'retryable': retryable,
|
|
115
|
+
'source': source,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
@@ -524,13 +524,17 @@ Map<String, Object?> readJsonMapOrEmpty(
|
|
|
524
524
|
/// container (the flat projection re-parsed it) is returned as-is. A bare scalar (int
|
|
525
525
|
/// from a spike path) is returned unchanged.
|
|
526
526
|
Object? _decodeJsonMapValue(Object? val) {
|
|
527
|
+
// ONLY the kernel's structural entry shape `{value: {Str|Int: ...}, stamp: ...}`
|
|
528
|
+
// may be unwrapped. Keying the check on `value` alone ate any legitimate
|
|
529
|
+
// domain object that happens to carry a `value` field (a custom-field entry
|
|
530
|
+
// `{fieldId, status, updatedAt, value}` lost everything but its value).
|
|
527
531
|
if (val is Map && val.containsKey('value')) {
|
|
528
532
|
final value = val['value'];
|
|
529
533
|
if (value is Map && value.containsKey('Str')) {
|
|
530
534
|
return _maybeJsonDecode(value['Str']);
|
|
531
535
|
}
|
|
532
536
|
if (value is Map && value.containsKey('Int')) return value['Int'];
|
|
533
|
-
return value;
|
|
537
|
+
if (val.containsKey('stamp')) return value;
|
|
534
538
|
}
|
|
535
539
|
return _maybeJsonDecode(val);
|
|
536
540
|
}
|
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. Typed protobuf and USDA boundary values 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.7
|
|
13
13
|
|
|
14
14
|
environment:
|
|
15
15
|
sdk: ^3.5.0
|
package/package.json
CHANGED