@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 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 catalogue, locally-born children).
6
- /// Each workspace is a LOCAL githolon — cloud sync is an optional per-workspace posture, not a prerequisite.
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; `bridge.session(name)` returns its handle. (Opening a born
776
- /// child as its own live session across a colocated cluster is gated on the shared engine plane, #42.)
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
- /// the signed envelope `{ ok, rows, attestation:{ source_genesis, … } }` — hand it back as an
1194
- /// [offerDirective] `attestations` element, or read `attestation.source_genesis` to enrol the source in a
1195
- /// reader's allow-list. [args] are the query params. [session] is the caller/reader session (defaults to
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). The returned `entry` is `{queryId, result, attestation:{source_genesis, public_key,
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.36.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
- // THE NAMED GAP (task #42): opening the born child as a SIBLING SESSION (`bridge.session(child)` → typed
15
- // query) requires the SHARED ENGINE PLANE — today each runner session is its own single-workspace engine, so
16
- // a sibling session can't see the child custody born in another. We do NOT fake that loop; the child handle
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
- // NAMED GAP (#42): the child handle is addressable on the plane, but opening it as its own session
97
- // across the runner's per-session engines needs the shared engine plane — we stop at the handle.
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.57.1",
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",
@@ -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;