@githolon/dsl 0.57.1 → 0.58.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.
|
@@ -2,8 +2,15 @@
|
|
|
2
2
|
/// `package:nomos_client/nomos_client.dart` barrel (which stays web-safe).
|
|
3
3
|
///
|
|
4
4
|
/// MULTI-WORKSPACE BY DESIGN. A [NomosBridge] is a device's LOCAL HOLARCHY PLANE: one bridge / one
|
|
5
|
-
/// transport, and MANY live workspaces on it (the home, each estate, each
|
|
6
|
-
///
|
|
5
|
+
/// transport / ONE shared engine plane, and MANY live workspaces mounted on it (the home, each estate, each
|
|
6
|
+
/// catalogue, locally-born children) — N workspaces on one wasm engine, not N engines. Each workspace is a
|
|
7
|
+
/// LOCAL githolon — cloud sync is an optional per-workspace posture, not a prerequisite.
|
|
8
|
+
///
|
|
9
|
+
/// LOCAL BIRTHS OPEN IN-PLACE. A `.births()` directive offered into one workspace writes the child's custody
|
|
10
|
+
/// straight into this same plane; the child is resident the moment it is born, so `bridge.connect(session:
|
|
11
|
+
/// child)` (no `restoreFrom`) mounts it IN-PLACE — no re-clone — and reads/writes flow into the born child at
|
|
12
|
+
/// once. (Proof: dsl/dart/test/cluster_births_test.dart; JS twin: cloud/web-client/test/realm.e2e.mjs.)
|
|
13
|
+
///
|
|
7
14
|
/// Open workspaces and hold their [NomosSession] handles:
|
|
8
15
|
///
|
|
9
16
|
/// ```dart
|
|
@@ -772,8 +772,9 @@ class NomosOfferOutcome {
|
|
|
772
772
|
final List<NomosCreated> created;
|
|
773
773
|
|
|
774
774
|
/// The born child workspace name(s), when the offer ran a `.births()` directive (else empty). Each is a
|
|
775
|
-
/// LOCAL workspace on this device's plane
|
|
776
|
-
///
|
|
775
|
+
/// LOCAL workspace on this device's plane, resident in the shared engine plane the moment it is born:
|
|
776
|
+
/// `bridge.session(name)` returns its handle, and `bridge.connect(session: name)` (no `restoreFrom`) mounts
|
|
777
|
+
/// it IN-PLACE and opens it as a live sibling session — reads/writes flow into the child at once (#42).
|
|
777
778
|
final List<String> born;
|
|
778
779
|
|
|
779
780
|
/// The raw per-child birth outcomes the holon reported (`{ workspace, genesisHead, … }`), when present.
|
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.37.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -10,11 +10,14 @@
|
|
|
10
10
|
// step-0 `bootstrap/installDomain`. Without it the birth failed with "bare JavaScript is not a domain
|
|
11
11
|
// package" (see cloud/web-client/src/kernel_provider_wasm.mjs boot).
|
|
12
12
|
// 3. A FAILED birth carries a TYPED reason to Dart (deliberate bad framework hash) — never a silent drop.
|
|
13
|
+
// 4. THE LOOP CLOSES (task #42): the born child OPENS as a sibling session on the SAME bridge and a typed
|
|
14
|
+
// query INSIDE it returns the genesis-seeded row. This is the whole point — a local birth is not just
|
|
15
|
+
// observable, it is USABLE: the child custody born in the parent session is resident in the shared
|
|
16
|
+
// engine plane, so `bridge.connect(session: born)` mounts it IN-PLACE (no re-clone) and reads its seed.
|
|
13
17
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// is addressable, and we stop there.
|
|
18
|
+
// (HISTORICAL: this leg was once a NAMED GAP — before the runner ran openRealm, each session was its own
|
|
19
|
+
// single-workspace engine, so a sibling session could not see custody born in another. The shared engine
|
|
20
|
+
// plane closed it; the JS twin of this proof is cloud/web-client/test/realm.e2e.mjs T2.)
|
|
18
21
|
@TestOn('vm')
|
|
19
22
|
library;
|
|
20
23
|
|
|
@@ -93,8 +96,8 @@ void main() {
|
|
|
93
96
|
expect(outcome.birthOutcomes.first['workspace'], outcome.born.single);
|
|
94
97
|
expect(outcome.birthOutcomes.first['status'], isNotNull);
|
|
95
98
|
|
|
96
|
-
//
|
|
97
|
-
//
|
|
99
|
+
// The child handle is addressable on the plane (the loop that OPENS + reads it is the dedicated test
|
|
100
|
+
// below — 'born child OPENS as a sibling session ...').
|
|
98
101
|
expect(harness.bridge.session(outcome.born.single).workspace, outcome.born.single);
|
|
99
102
|
} finally {
|
|
100
103
|
await harness.dispose();
|
|
@@ -121,6 +124,46 @@ void main() {
|
|
|
121
124
|
}
|
|
122
125
|
}, timeout: const Timeout(Duration(minutes: 3)));
|
|
123
126
|
|
|
127
|
+
test('born child OPENS as a sibling session on the SAME bridge → typed query returns the seeded row (THE LOOP)',
|
|
128
|
+
() async {
|
|
129
|
+
final skip = _missingPrereq();
|
|
130
|
+
if (skip != null) {
|
|
131
|
+
markTestSkipped(skip);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
final (harness, outcome) = await _birth();
|
|
135
|
+
try {
|
|
136
|
+
expect(outcome.birthOutcomes.first['status'], 'born',
|
|
137
|
+
reason: 'precondition: the child is born on the local plane');
|
|
138
|
+
final child = outcome.born.single;
|
|
139
|
+
|
|
140
|
+
// THE LOOP CLOSES: open the born child as its OWN session on the SAME bridge. No `restoreFrom` — the
|
|
141
|
+
// child custody is ALREADY resident in the shared engine plane (born in the parent session), so the
|
|
142
|
+
// runner mounts it IN-PLACE (openRealm.workspace(child)), no re-clone. This is exactly realm.e2e T2.
|
|
143
|
+
await harness.bridge.connect(workspace: child, session: child);
|
|
144
|
+
|
|
145
|
+
// A typed query INSIDE the born child returns its genesis-seeded row — the estate the child's genesis
|
|
146
|
+
// chain seeded via estate/seedEstateWorkspace, keyed on the owner (`estate-<sha256(owner)>` self-bound).
|
|
147
|
+
final rows = await harness.bridge.session(child).query('estatesByOwner', {'owner': _owner});
|
|
148
|
+
expect(rows, hasLength(1),
|
|
149
|
+
reason: 'the born child\'s genesis seed reads back through its OWN session (the loop is closed)');
|
|
150
|
+
expect(rows.first['data']?['owner'], _owner,
|
|
151
|
+
reason: 'the seeded EstateOwner belongs to the birth\'s owner');
|
|
152
|
+
expect(rows.first['data']?['estateName'], child,
|
|
153
|
+
reason: 'the estate self-committed its own workspace name (estate-<sha256(owner)>)');
|
|
154
|
+
|
|
155
|
+
// listWorkspaces now enumerates BOTH the parent and the newly-opened born child on the one plane.
|
|
156
|
+
final listed = await harness.bridge.listWorkspaces();
|
|
157
|
+
final sessions = listed.map((w) => w['session'].toString()).toSet();
|
|
158
|
+
expect(sessions, contains(child),
|
|
159
|
+
reason: 'listWorkspaces surfaces the born child as a live session');
|
|
160
|
+
expect(sessions, contains('estate-birth'),
|
|
161
|
+
reason: 'the parent workspace is still listed alongside its born child');
|
|
162
|
+
} finally {
|
|
163
|
+
await harness.dispose();
|
|
164
|
+
}
|
|
165
|
+
}, timeout: const Timeout(Duration(minutes: 3)));
|
|
166
|
+
|
|
124
167
|
test('birth FAILURE carries a typed reason to Dart (deliberate bad framework hash)', () async {
|
|
125
168
|
final skip = _missingPrereq();
|
|
126
169
|
if (skip != null) {
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// SCALE SANITY — DART LEG (task #42). "A device holds dozens of live workspaces" is a TESTED truth here,
|
|
2
|
+
// not a slogan: one bridge / one runner / ONE shared engine plane mounts ~20 local-only workspaces, each with
|
|
3
|
+
// the SAME tenant law installed offline (one compiled fixture's deploy.json, reused N times — same law, N
|
|
4
|
+
// distinct workspaces), then writes + reads each in isolation and asserts `listWorkspaces` surfaces all 20.
|
|
5
|
+
//
|
|
6
|
+
// This is the colocation architecture the shared engine plane (openRealm) exists for: N workspaces on ONE
|
|
7
|
+
// wasm engine, not N engines. The suite is deliberately DOZENS (CI-friendly), not hundreds — enough to pin
|
|
8
|
+
// the "many live workspaces per device" claim while completing in sane wall-clock time (asserted below).
|
|
9
|
+
@TestOn('vm')
|
|
10
|
+
library;
|
|
11
|
+
|
|
12
|
+
import 'dart:io';
|
|
13
|
+
|
|
14
|
+
import 'package:nomos_client/nomos_headless.dart';
|
|
15
|
+
import 'package:test/test.dart';
|
|
16
|
+
|
|
17
|
+
String get _root => '${Directory.current.path}/../..';
|
|
18
|
+
String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
|
|
19
|
+
String get _guestbookDeploy => '$_root/examples/guestbook/build/guestbook.deploy.json';
|
|
20
|
+
String get _runtimeDir => '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
|
|
21
|
+
|
|
22
|
+
/// How many colocated workspaces to open on the one plane. Dozens (CI-friendly), tunable via env for a
|
|
23
|
+
/// heavier local soak (`NOMOS_SCALE_N=100 dart test test/cluster_scale_test.dart`).
|
|
24
|
+
int get _n => int.tryParse(Platform.environment['NOMOS_SCALE_N'] ?? '') ?? 20;
|
|
25
|
+
|
|
26
|
+
String? _missingPrereq() {
|
|
27
|
+
if (!File(_repoEntry).existsSync()) return 'in-repo runner not found at $_repoEntry';
|
|
28
|
+
if (!File(_guestbookDeploy).existsSync()) {
|
|
29
|
+
return 'compile the guestbook example: cd examples/guestbook && npx nomos-compile — missing $_guestbookDeploy';
|
|
30
|
+
}
|
|
31
|
+
if (!File('$_runtimeDir/holon.wasm').existsSync() ||
|
|
32
|
+
!File('$_runtimeDir/packages.json').existsSync()) {
|
|
33
|
+
return 'warm ~/.holon/runtime first (run any `githolon` command once)';
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
void main() {
|
|
39
|
+
test('a device holds dozens of live workspaces on ONE engine plane — write + read each, all listed',
|
|
40
|
+
() async {
|
|
41
|
+
final skip = _missingPrereq();
|
|
42
|
+
if (skip != null) {
|
|
43
|
+
markTestSkipped(skip);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
final names = [for (var i = 0; i < _n; i++) 'ws-${i.toString().padLeft(2, '0')}'];
|
|
48
|
+
// Same law (one deploy.json), N distinct workspaces — the exact "one device, many workspaces" shape.
|
|
49
|
+
final workspaces = {for (final ws in names) ws: [_guestbookDeploy]};
|
|
50
|
+
|
|
51
|
+
final sw = Stopwatch()..start();
|
|
52
|
+
final harness = await NomosTestHarness.cluster(
|
|
53
|
+
workspaces: workspaces,
|
|
54
|
+
nodeEntry: _repoEntry,
|
|
55
|
+
runtimeDir: _runtimeDir,
|
|
56
|
+
// Installing + mounting N workspaces takes longer than the single-session default; give it headroom.
|
|
57
|
+
readyTimeout: const Duration(seconds: 90),
|
|
58
|
+
);
|
|
59
|
+
final openMs = sw.elapsedMilliseconds;
|
|
60
|
+
try {
|
|
61
|
+
expect(harness.sessions.toSet(), names.toSet(),
|
|
62
|
+
reason: 'all $_n workspaces connected as sessions on the one bridge');
|
|
63
|
+
|
|
64
|
+
// WRITE one row into each workspace, keyed by its own name — so a cross-workspace read must come back
|
|
65
|
+
// EMPTY (isolation) and the own-workspace read must return exactly the one row (no bleed on the plane).
|
|
66
|
+
sw.reset();
|
|
67
|
+
for (final ws in names) {
|
|
68
|
+
await harness.session(ws).offerDirective(
|
|
69
|
+
domain: 'guestbook',
|
|
70
|
+
directiveId: 'signGuestbook',
|
|
71
|
+
payload: {
|
|
72
|
+
'author': ws,
|
|
73
|
+
'message': 'hello from $ws',
|
|
74
|
+
'mood': 'delighted',
|
|
75
|
+
'signedAt': '2026-07-05T00:00:00Z',
|
|
76
|
+
},
|
|
77
|
+
actor: ws,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
final writeMs = sw.elapsedMilliseconds;
|
|
81
|
+
|
|
82
|
+
// READ each back — the own author returns its single row; a foreign author returns nothing (isolation).
|
|
83
|
+
sw.reset();
|
|
84
|
+
for (final ws in names) {
|
|
85
|
+
final own = await harness.session(ws).query('entriesByAuthor', {'author': ws});
|
|
86
|
+
expect(own, hasLength(1), reason: '$ws holds exactly its own 1 row');
|
|
87
|
+
expect(own.first['data']?['message'], 'hello from $ws');
|
|
88
|
+
|
|
89
|
+
final foreign = names.firstWhere((o) => o != ws);
|
|
90
|
+
final cross = await harness.session(ws).query('entriesByAuthor', {'author': foreign});
|
|
91
|
+
expect(cross, isEmpty, reason: '$ws does NOT see $foreign\'s data — per-workspace isolation');
|
|
92
|
+
}
|
|
93
|
+
final readMs = sw.elapsedMilliseconds;
|
|
94
|
+
|
|
95
|
+
// listWorkspaces enumerates the whole local plane — every one of the N is present + mounted.
|
|
96
|
+
final listed = await harness.bridge.listWorkspaces();
|
|
97
|
+
expect(listed.map((w) => w['session'].toString()).toSet(), names.toSet(),
|
|
98
|
+
reason: 'listWorkspaces surfaces all $_n live workspaces');
|
|
99
|
+
expect(listed.every((w) => w['mounted'] == true), isTrue,
|
|
100
|
+
reason: 'every listed workspace is mounted');
|
|
101
|
+
|
|
102
|
+
// Wall-clock sanity — dozens of live workspaces must open + drive in a CI-sane budget, not minutes.
|
|
103
|
+
// Generous ceilings (CI is slow); the point is "seconds per workspace is a bug", not a micro-benchmark.
|
|
104
|
+
// ignore: avoid_print
|
|
105
|
+
print('[scale] $_n workspaces: open+install ${openMs}ms, '
|
|
106
|
+
'write ${writeMs}ms (${(writeMs / _n).toStringAsFixed(1)}ms/ws), '
|
|
107
|
+
'read ${readMs}ms (${(readMs / _n).toStringAsFixed(1)}ms/ws)');
|
|
108
|
+
expect(writeMs, lessThan(_n * 5000),
|
|
109
|
+
reason: 'per-workspace write stays well under a 5s/ws ceiling (got ${(writeMs / _n).round()}ms/ws)');
|
|
110
|
+
expect(readMs, lessThan(_n * 5000),
|
|
111
|
+
reason: 'per-workspace read stays well under a 5s/ws ceiling (got ${(readMs / _n).round()}ms/ws)');
|
|
112
|
+
} finally {
|
|
113
|
+
await harness.dispose();
|
|
114
|
+
}
|
|
115
|
+
// The whole suite (open + install + write + read + teardown of N workspaces) completes in sane time.
|
|
116
|
+
}, timeout: const Timeout(Duration(minutes: 10)));
|
|
117
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.58.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|