@githolon/dsl 0.57.1 → 0.58.1
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 +4 -0
- package/dart/lib/nomos_headless.dart +9 -2
- package/dart/lib/src/webview_bridge.dart +8 -8
- package/dart/pubspec.yaml +1 -1
- package/dart/test/cluster_births_test.dart +49 -6
- package/dart/test/cluster_scale_test.dart +117 -0
- package/package.json +1 -1
- package/src/codegen_proof.ts +15 -0
- package/src/law_check.ts +6 -0
package/dart/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## 0.37.1
|
|
2
|
+
|
|
3
|
+
- Doc fix: `NomosBridge.attestedRead` docs now match the runtime — the call answers `{ entry: {queryId, result, attestation: …} }`; pass `result['entry']` as one `offerDirective` `attestations` element.
|
|
4
|
+
|
|
1
5
|
## 0.32.0
|
|
2
6
|
|
|
3
7
|
- SPATIAL READS: `NomosBbox` + `NomosBridge.spatialWithin(spatialId, bbox)` + `watchSpatial(...)` — the R*Tree probe surfaces on the bridge; generated clients (tooling ≥0.46) emit typed `sitesByBounds({required NomosBbox bbox})` + watch accessors.
|
|
@@ -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.
|
|
@@ -1190,14 +1191,13 @@ class NomosBridge {
|
|
|
1190
1191
|
/// ATTESTED READ — serve + SIGN a declared query over a COLOCATED source session's certified fold (the
|
|
1191
1192
|
/// dynamic-source trust lane; architecture/dynamic_source_attested_reads.md). In a [NomosTestHarness.cluster]
|
|
1192
1193
|
/// (one engine plane, many sessions) this fulfils the read IN-PROCESS from the [from] session and returns
|
|
1193
|
-
///
|
|
1194
|
-
/// [offerDirective] `attestations` element, or read
|
|
1195
|
-
/// reader's allow-list. [args] are the
|
|
1196
|
-
/// this view's bound session).
|
|
1194
|
+
/// `{ entry: {queryId, result, attestation:{source_genesis, public_key, …}} }` — pass `result['entry']`
|
|
1195
|
+
/// (the whole entry map) as one [offerDirective] `attestations` element, or read
|
|
1196
|
+
/// `entry['attestation']['source_genesis']` to enrol the source in a reader's allow-list. [args] are the
|
|
1197
|
+
/// query params. [session] is the caller/reader session (defaults to this view's bound session).
|
|
1197
1198
|
/// [attestorSecret] is the source's base64 read-attestor secret (from [authorKeygen]); when omitted the
|
|
1198
1199
|
/// source session's connect-time `authorSecret` signs. [nowMs] stamps the freshness clock (defaults to the
|
|
1199
|
-
/// source's own clock).
|
|
1200
|
-
/// …}}` — pass the whole `entry` as one [offerDirective] `attestations` element.
|
|
1200
|
+
/// source's own clock).
|
|
1201
1201
|
Future<Map<String, Object?>> attestedRead({
|
|
1202
1202
|
required String from,
|
|
1203
1203
|
required String queryId,
|
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.1
|
|
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.1",
|
|
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",
|
package/src/codegen_proof.ts
CHANGED
|
@@ -1499,6 +1499,21 @@ export function withBirthProbeGlobals<T>(run: () => T): T {
|
|
|
1499
1499
|
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 16777619) >>> 0; }
|
|
1500
1500
|
return h.toString(16).padStart(8, "0").repeat(8);
|
|
1501
1501
|
},
|
|
1502
|
+
// A read STUB so a plan with a declared read()/read(…, {from}) PROBES cleanly instead of throwing
|
|
1503
|
+
// "nomos.read is not provided" (which twin A would mis-report as a payload-dependent throw with an
|
|
1504
|
+
// "add .reads(…)" hint — misleading for a correctly declared .readsFrom edge). Local reads answer no
|
|
1505
|
+
// rows; foreign reads answer the {rows, attestation} wrapper the read() lane demands. The UNDECLARED
|
|
1506
|
+
// read()/edge cases stay caught by their real gates (the compile lump-marker grep + attestedReadFindings).
|
|
1507
|
+
read: (_q: string, _a: Record<string, string>, o?: { from?: string }) =>
|
|
1508
|
+
o?.from !== undefined && o.from !== ""
|
|
1509
|
+
? {
|
|
1510
|
+
rows: [],
|
|
1511
|
+
attestation: {
|
|
1512
|
+
source_genesis: "probe", source_head: "probe", source_epoch: 0,
|
|
1513
|
+
attested_at_ms: 0, public_key: "", signature: "", result_hash: "",
|
|
1514
|
+
},
|
|
1515
|
+
}
|
|
1516
|
+
: [],
|
|
1502
1517
|
};
|
|
1503
1518
|
// RELATION-NAMESPACING: a probed seed plan may build a relation OBJECT TOKEN (`selfWorkspace()`),
|
|
1504
1519
|
// which qualifies its type by the dispatching domain. The proof generator's STATIC probe reads the
|
package/src/law_check.ts
CHANGED
|
@@ -379,6 +379,12 @@ function unreadPayloadFindings(domain: string, dir: Directive<unknown>, probe: P
|
|
|
379
379
|
const objectFromField = (dir as { requiresRelationGate?: { objectFrom?: { idField?: string } } })
|
|
380
380
|
.requiresRelationGate?.objectFrom?.idField;
|
|
381
381
|
if (typeof objectFromField === "string") gateConsumed.add(objectFromField);
|
|
382
|
+
// A dynamic .readsFrom(fromPayload("<wsField>"), …) edge routes the foreign read by a payload field the
|
|
383
|
+
// ROUTING lane consumes (the plan may pass it straight to read(…, {from}) without a tracked property
|
|
384
|
+
// read on the sampled path) — a declared trust-edge field is never a dead field.
|
|
385
|
+
for (const e of (dir as { declaredForeignReads?: ForeignReadEdge[] }).declaredForeignReads ?? []) {
|
|
386
|
+
if ("fromPayload" in e && typeof e.fromPayload === "string") gateConsumed.add(e.fromPayload);
|
|
387
|
+
}
|
|
382
388
|
const out: Finding[] = [];
|
|
383
389
|
for (const f of requiredPayloadFields(dir.payloadSchema as z.ZodTypeAny)) {
|
|
384
390
|
if (probe.readFields.has(f)) continue;
|