@githolon/dsl 0.89.0 → 0.90.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.
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // ```dart
7
7
  // final cloud = await NomosTestCloud.start(serverEntry: devServerPath); // spawns the dev cloud + proxy
8
- // final harness = await NomosTestHarness.cluster(cloud: cloud.origin, ...); // cloud-BOUND sessions
8
+ // final harness = await NomosTestHarness.cloudBound(cloud: cloud, deployJsonPaths: [...], platformWorkspace: 'co2-platform', subject: 'user:test');
9
9
  // // ... author offline ...
10
10
  // cloud.setOnline(false); // PARTITION — the up leg can't reach custody
11
11
  // await harness.session('estate').sync(admit: true); // stays local; nothing lands remotely
@@ -33,6 +33,9 @@
33
33
  import 'dart:convert';
34
34
  import 'dart:io';
35
35
 
36
+ import 'package:crypto/crypto.dart' show sha256;
37
+
38
+ import 'dev_cloud.dart';
36
39
  import 'node_runner_transport.dart';
37
40
  import 'webview_bridge.dart' show NomosBridge, NomosSession;
38
41
 
@@ -147,6 +150,25 @@ class NomosTestHarness {
147
150
  /// When set, overrides EVERY phase's default deadline (see [HarnessPhaseTimeout]).
148
151
  final Duration? _phaseTimeout;
149
152
 
153
+ /// The test cloud a [cloudBound] harness runs against (null for the offline constructors). Partition it
154
+ /// with [NomosTestCloud.setOnline]; assert remote custody with [NomosTestCloud.remoteHead].
155
+ final NomosTestCloud? testCloud;
156
+
157
+ /// The HOME workspace a [cloudBound] harness mounted (born remotely by the platform ceremony). Null for
158
+ /// the offline constructors.
159
+ final String? homeWorkspace;
160
+
161
+ /// The mounted home as a [NomosSession] (the [cloudBound] app-shaped entry point — bind the generated
162
+ /// `HomeClient` to it). Throws for an offline harness (no home ceremony ran).
163
+ NomosSession get homeSession {
164
+ final ws = homeWorkspace;
165
+ if (ws == null) {
166
+ throw NomosTestHarnessException(
167
+ 'homeSession is only available on a cloudBound harness (this one was built by ephemeral/cluster)');
168
+ }
169
+ return session(ws);
170
+ }
171
+
150
172
  NomosTestHarness._(this.bridge, this.workspace, this._transport, this._workdir,
151
173
  this._onDiagnostic, List<String> sessionNames,
152
174
  {Map<String, String> devicePublics = const {},
@@ -154,7 +176,9 @@ class NomosTestHarness {
154
176
  Map<String, List<String>> lawHashes = const {},
155
177
  Map<String, String> snapshots = const {},
156
178
  _InstallLane? installLane,
157
- Duration? phaseTimeout})
179
+ Duration? phaseTimeout,
180
+ this.testCloud,
181
+ this.homeWorkspace})
158
182
  : _sessionNames = List.of(sessionNames), // GROWABLE — restoreWorkspace may add a fresh session
159
183
  _devicePublics = devicePublics,
160
184
  _lawHashes = Map.of(lawHashes),
@@ -613,6 +637,226 @@ class NomosTestHarness {
613
637
  }
614
638
  }
615
639
 
640
+ /// CLOUD-BOUND HARNESS (co2's real-app topology): a [NomosTestCloud] plays the remote control plane, the
641
+ /// REAL authenticated home-birth ceremony runs REMOTELY on [platformWorkspace], and the born home is
642
+ /// mounted into ONE local engine plane as a normal cloud-bound [NomosSession] ([homeSession]). From there
643
+ /// the app shape holds exactly: generated `.births()` directives run LOCALLY (the app never clones the
644
+ /// platform), children open as sibling sessions (`outcome.bornRefs.single.acquire(harness.bridge)`),
645
+ /// writes/projections work offline, and [converge] establishes remote custody parent-first when
646
+ /// connectivity returns ([NomosTestCloud.setOnline]). Entirely inside the test cloud's trust universe.
647
+ ///
648
+ /// ```dart
649
+ /// final cloud = await NomosTestCloud.start(serverEntry: devServer);
650
+ /// final h = await NomosTestHarness.cloudBound(
651
+ /// cloud: cloud, deployJsonPaths: ['build/co2.deploy.json'],
652
+ /// platformWorkspace: 'co2-platform', subject: 'user:test');
653
+ /// final home = h.homeSession; // the app's entry point
654
+ /// cloud.setOnline(false); // PARTITION
655
+ /// // ... local births + writes ...
656
+ /// cloud.setOnline(true);
657
+ /// await h.converge(); // home first, then locally-born children
658
+ /// expect((await cloud.remoteHead(home.workspace)).initialized, isTrue);
659
+ /// ```
660
+ static Future<NomosTestHarness> cloudBound({
661
+ required NomosTestCloud cloud,
662
+ required List<String> deployJsonPaths,
663
+ String platformWorkspace = 'test-platform',
664
+ required String subject,
665
+ String? authToken,
666
+ String clientId = 'test-runner',
667
+ String? nodeEntry,
668
+ String? resolveFrom,
669
+ String node = 'node',
670
+ bool autoTearDown = true,
671
+ void Function(Future<void> Function())? registerTearDown,
672
+ void Function(String line)? onDiagnostic,
673
+ Duration readyTimeout = const Duration(seconds: 30),
674
+ Duration connectTimeout = const Duration(seconds: 120),
675
+ Duration? phaseTimeout,
676
+ bool decoySession = true,
677
+ }) async {
678
+ if (deployJsonPaths.isEmpty) {
679
+ throw NomosTestHarnessException(
680
+ 'no deployJsonPaths — pass the build/<name>.deploy.json `npx githolon compile` emits');
681
+ }
682
+ final entry = nodeEntry ??
683
+ await _resolveEntry(node, resolveFrom ?? Directory.current.path);
684
+ final bareUid = subject.startsWith('user:') ? subject.substring(5) : subject;
685
+ final http = HttpClient();
686
+ Future<Map<String, Object?>> post(String path, Object? body,
687
+ {Map<String, String> headers = const {}}) async {
688
+ final req = await http.postUrl(Uri.parse('${cloud.origin}$path'));
689
+ req.headers.set('content-type', 'application/json');
690
+ req.headers.set('x-nomos-principal', bareUid);
691
+ if (authToken != null) req.headers.set('x-nomos-auth', authToken);
692
+ headers.forEach(req.headers.set);
693
+ if (body != null) req.add(utf8.encode(body is String ? body : jsonEncode(body)));
694
+ final resp = await req.close();
695
+ final text = await resp.transform(utf8.decoder).join();
696
+ final decoded = (jsonDecode(text) as Map).cast<String, Object?>();
697
+ if (resp.statusCode >= 300 || decoded['ok'] == false) {
698
+ throw NomosTestHarnessException(
699
+ 'cloudBound setup: POST $path → ${resp.statusCode}: ${text.substring(0, text.length.clamp(0, 400))}');
700
+ }
701
+ return decoded;
702
+ }
703
+
704
+ final workdir = await Directory.systemTemp.createTemp('nomos-harness-');
705
+ NodeRunnerTransport? transport;
706
+ try {
707
+ // 1/5 — the remote control plane: create the platform workspace + deploy the law under the test
708
+ // cloud's own trust universe (its K_root; nothing here touches production).
709
+ String? frameworkHash;
710
+ final lawHashes = <String>[];
711
+ await _phased(
712
+ phase: '1/5: create platform + deploy law on the test cloud',
713
+ workspace: platformWorkspace,
714
+ awaiting: 'POST /v2/workspaces + /domains',
715
+ timeout: phaseTimeout ?? _kInstallDeadline,
716
+ onDiagnostic: onDiagnostic,
717
+ run: () async {
718
+ final created = await post('/v2/workspaces/$platformWorkspace', null);
719
+ frameworkHash =
720
+ ((created['controller'] as Map?)?['domainHash'])?.toString();
721
+ for (final p in deployJsonPaths) {
722
+ final body = await File(p).readAsString();
723
+ await post('/v2/workspaces/$platformWorkspace/domains', body);
724
+ final summary = ((jsonDecode(body) as Map)['summary'] as Map?);
725
+ final h = summary?['domainHash']?.toString();
726
+ if (h != null && h.isNotEmpty) lawHashes.add(h);
727
+ }
728
+ });
729
+ if (frameworkHash == null || frameworkHash!.isEmpty) {
730
+ throw NomosTestHarnessException(
731
+ 'cloudBound: the test cloud returned no controller.domainHash — cannot run the birth ceremony (frameworkHash unknown)');
732
+ }
733
+ if (lawHashes.isEmpty) {
734
+ throw NomosTestHarnessException(
735
+ 'cloudBound: no deploy.json carried a summary.domainHash — cannot run the birth ceremony (lawHash unknown)');
736
+ }
737
+ // 2/5 — THE HOME CEREMONY, remotely: the platform's own gateway offer births the home (the ONE
738
+ // control-plane contact; the app lane below never touches the platform again).
739
+ String? home;
740
+ await _phased(
741
+ phase: '2/5: remote home-birth ceremony',
742
+ workspace: platformWorkspace,
743
+ awaiting: 'POST /v2/workspaces/$platformWorkspace/birthHome',
744
+ timeout: phaseTimeout ?? _kInstallDeadline,
745
+ onDiagnostic: onDiagnostic,
746
+ run: () async {
747
+ final out = await post('/v2/workspaces/$platformWorkspace/birthHome', {
748
+ 'owner': subject,
749
+ 'frameworkHash': frameworkHash,
750
+ 'lawHash': lawHashes.first,
751
+ 'bornAt': DateTime.now().toUtc().toIso8601String(),
752
+ });
753
+ final born = (out['born'] as List?) ?? const [];
754
+ final bornNames = born.map((b) => (b as Map)['workspace']?.toString()).whereType<String>().where((w) => w.isNotEmpty).toList();
755
+ home = bornNames.isEmpty ? null : bornNames.first;
756
+ home ??= _homeNameOf(subject); // deterministic fallback (home-<sha256(subject)[..40]>)
757
+ });
758
+ // 3/5 — the LOCAL engine plane, CLOUD-BOUND (no offline env: the runner pulls the runtime + the
759
+ // home's custody from the test cloud like a real device).
760
+ transport = await _phased(
761
+ phase: '3/5: spawn the node runner (cloud-bound)',
762
+ awaiting: 'Process.start($node)',
763
+ timeout: phaseTimeout ?? _kSpawnDeadline,
764
+ onDiagnostic: onDiagnostic,
765
+ run: () => NodeRunnerTransport.start(
766
+ entry: entry, resolveFrom: resolveFrom, node: node, onDiagnostic: onDiagnostic));
767
+ final plane = NomosBridge(transport!);
768
+ await _phased(
769
+ phase: '4/5: runner ready',
770
+ awaiting: "event:'ready'",
771
+ timeout: phaseTimeout ?? readyTimeout,
772
+ onDiagnostic: onDiagnostic,
773
+ run: () => plane.ready);
774
+ // 5/5 — mount the born home as a normal cloud-bound session (clone from the test cloud).
775
+ await _phased(
776
+ phase: '5/5: mount the born home',
777
+ workspace: home,
778
+ awaiting: 'connect(cloud:, workspace: $home)',
779
+ timeout: phaseTimeout ?? connectTimeout,
780
+ onDiagnostic: onDiagnostic,
781
+ run: () => plane.connect(
782
+ cloud: cloud.origin,
783
+ workspace: home!,
784
+ clientId: clientId,
785
+ authToken: authToken,
786
+ ));
787
+ // NO DECOY on a cloudBound harness: the app is genuinely multi-workspace (the home plus every
788
+ // locally-born child), so the plane is never sole-session — the decoy the offline harness
789
+ // needs (to break single-session inference) would just be a spurious local-only workspace here.
790
+ final _ = decoySession; // (accepted for signature parity; a cloudBound plane needs no decoy)
791
+ final harness = NomosTestHarness._(
792
+ plane.session(home!),
793
+ home!,
794
+ transport,
795
+ workdir,
796
+ onDiagnostic,
797
+ [home!],
798
+ frameworkHash: frameworkHash,
799
+ lawHashes: {home!: lawHashes},
800
+ phaseTimeout: phaseTimeout,
801
+ testCloud: cloud,
802
+ homeWorkspace: home,
803
+ );
804
+ if (autoTearDown && registerTearDown != null) {
805
+ registerTearDown(() => harness.dispose());
806
+ }
807
+ return harness;
808
+ } catch (e) {
809
+ try { transport?.dispose(); } catch (_) {}
810
+ try { await workdir.delete(recursive: true); } catch (_) {}
811
+ rethrow;
812
+ } finally {
813
+ http.close(force: true);
814
+ }
815
+ }
816
+
817
+ /// The framework home-slot derivation (`home-<sha256(subject)[0..40]>`) — the fallback when the birth
818
+ /// offer response carries no `born` surface (an older dev cloud).
819
+ static String _homeNameOf(String subject) =>
820
+ 'home-${sha256.convert(utf8.encode(subject)).toString().substring(0, 40)}';
821
+
822
+ /// CONVERGE local custody with the test cloud (co2 topology step 8): sync every connected session —
823
+ /// PARENT-FIRST by connect order, so the home's chain lands remotely and its birth offer-effects create
824
+ /// each locally-born child's remote custody BEFORE that child's own sync converges against it. Repeats
825
+ /// until every session reports neither `pushFailed` nor `stillAhead`, or [timeout] (then throws with the
826
+ /// per-workspace last verdicts). Returns the final sync result per workspace.
827
+ Future<Map<String, Map<String, Object?>>> converge(
828
+ {Duration timeout = const Duration(seconds: 120), List<String>? workspaces}) async {
829
+ // Every LIVE session on the plane (not the harness's construction-time list — a locally-born child
830
+ // acquired after construction must converge too), PARENT-FIRST: the home syncs before its children, so
831
+ // its birth offer-effect creates each child's remote custody BEFORE that child pushes its own writes.
832
+ final live = bridge.connectedSessions.where((w) => w != _decoyWorkspace).toList();
833
+ final hw = homeWorkspace;
834
+ if (hw != null && live.remove(hw)) live.insert(0, hw); // the parent leads
835
+ final targets = workspaces ?? live;
836
+ final last = <String, Map<String, Object?>>{};
837
+ final pending = List.of(targets);
838
+ final deadline = DateTime.now().add(timeout);
839
+ while (pending.isNotEmpty) {
840
+ for (final ws in List.of(pending)) {
841
+ try {
842
+ final r = (await bridge.session(ws).sync(admit: true)).cast<String, Object?>();
843
+ last[ws] = r;
844
+ if (r['pushFailed'] != true && r['stillAhead'] != true) pending.remove(ws);
845
+ } catch (e) {
846
+ last[ws] = {'error': e.toString()};
847
+ }
848
+ }
849
+ if (pending.isEmpty) break;
850
+ if (DateTime.now().isAfter(deadline)) {
851
+ throw NomosTestHarnessException(
852
+ 'converge: ${pending.length} workspace(s) did not converge within ${timeout.inSeconds}s — '
853
+ '${pending.map((w) => '$w: ${jsonEncode(last[w] ?? {})}').join('; ')}');
854
+ }
855
+ await Future<void>.delayed(const Duration(milliseconds: 500));
856
+ }
857
+ return last;
858
+ }
859
+
616
860
  // ── CUSTODY DIAGNOSTIC HELPERS (architecture/session_bound_clients.md ask 9) ──
617
861
 
618
862
  /// The raw multi-workspace plane under [bridge] (the session handle a single-workspace harness
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.56.0
14
+ version: 0.57.0
15
15
 
16
16
  environment:
17
17
  sdk: ^3.5.0
@@ -0,0 +1,140 @@
1
+ // THE CLOUD-BOUND TOPOLOGY ACCEPTANCE (co2's exact 10-step ask) — the real-app shape end-to-end:
2
+ //
3
+ // 1. the remote platform births the home (the ONE control-plane contact — the authenticated ceremony);
4
+ // 2. the app mounts the home locally (a normal cloud-bound NomosSession; co2-platform is never cloned);
5
+ // 3. the network PARTITIONS (NomosTestCloud.setOnline(false));
6
+ // 4. the home births the estate LOCALLY (`birthEstateWorkspace` — a `.births()` offer on this device);
7
+ // 5. the estate opens as a sibling session and ACCEPTS LOCAL WRITES while offline;
8
+ // 6. UI-facing queries return those writes immediately (no sync awaited, ever);
9
+ // 7. the network HEALS;
10
+ // 8. `harness.converge()` establishes remote custody PARENT-FIRST (the home's synced chain folds the
11
+ // birth offer-effect remotely, creating the estate's custody; then the estate converges against it);
12
+ // 9. a FRESH observer (the dev cloud's own remote projection) sees the estate writes;
13
+ // 10. custody is REAL on both: remote heads born with commits (the dev cloud adopted the chains through
14
+ // the same gate everything else uses — replay IS validation).
15
+ //
16
+ // Entirely inside the test cloud's trust universe: nothing here touches production K_root or custody.
17
+ import 'dart:convert';
18
+ import 'dart:io';
19
+
20
+ import 'package:nomos_client/nomos_headless.dart';
21
+ import 'package:test/test.dart';
22
+
23
+ String get _root => '${Directory.current.path}/../..';
24
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
25
+ String get _devServer => '$_root/cloud/dev-server/server.mjs';
26
+ String get _deploy =>
27
+ '$_root/bench/scale/fixtures/cloud-topology/build/cloud-topology.deploy.json';
28
+ const _subject = 'user:topology-test';
29
+
30
+ String? _missingPrereq() {
31
+ if (!File(_repoEntry).existsSync()) return 'in-repo runner not found at $_repoEntry';
32
+ if (!File(_devServer).existsSync()) return 'in-repo dev server not found at $_devServer';
33
+ if (!File(_deploy).existsSync()) {
34
+ return 'compile the fixture: cd bench/scale/fixtures/cloud-topology && node ../../../../dsl/compile_package.mjs — missing $_deploy';
35
+ }
36
+ return null;
37
+ }
38
+
39
+ void main() {
40
+ test('the co2 topology: remote home ceremony → local estate birth offline → converge → remote custody',
41
+ () async {
42
+ final skip = _missingPrereq();
43
+ if (skip != null) {
44
+ markTestSkipped(skip);
45
+ return;
46
+ }
47
+ final diag = Platform.environment['NOMOS_HARNESS_DIAG'] == '1'
48
+ ? (String l) => stderr.writeln(l)
49
+ : null;
50
+ final cloud = await NomosTestCloud.start(serverEntry: _devServer, onDiagnostic: diag);
51
+ NomosTestHarness? harness;
52
+ try {
53
+ // 1+2 — the remote ceremony + the local mount, in one constructor (the app's boot path).
54
+ harness = await NomosTestHarness.cloudBound(
55
+ cloud: cloud,
56
+ deployJsonPaths: [_deploy],
57
+ platformWorkspace: 'topology-platform',
58
+ subject: _subject,
59
+ nodeEntry: _repoEntry,
60
+ onDiagnostic: diag,
61
+ );
62
+ final home = harness.homeSession;
63
+ expect(home.workspace, startsWith('home-'),
64
+ reason: 'the remote ceremony born the deterministic home slot');
65
+ final remoteHome = await cloud.remoteHead(home.workspace);
66
+ expect(remoteHome.initialized, isTrue,
67
+ reason: 'the home was born REMOTELY (custody exists before the app ever mounted it)');
68
+
69
+ // 3 — PARTITION.
70
+ cloud.setOnline(false);
71
+
72
+ // 4 — the home births the estate LOCALLY (the app lane never needs the network).
73
+ final outcome = await harness.bridge.session(home.workspace).offerCreates(
74
+ domain: 'home',
75
+ directiveId: 'birthEstateWorkspace',
76
+ payload: {
77
+ 'owner': _subject,
78
+ 'frameworkHash': harness.frameworkHash,
79
+ 'lawHash': harness.lawHashes(home.workspace).first,
80
+ 'bornAt': '2026-07-14T00:00:00Z',
81
+ },
82
+ actor: _subject,
83
+ );
84
+ expect(outcome.born, isNotEmpty, reason: 'the local .births() offer spawned the estate');
85
+ final estateWs = outcome.born.single;
86
+ expect(estateWs, startsWith('estate-'));
87
+
88
+ // 5 — the estate opens as a sibling session and accepts a LOCAL WRITE while offline.
89
+ final estate = await harness.bridge.acquireSession(estateWs);
90
+ await estate.offerCreates(
91
+ domain: 'estate',
92
+ directiveId: 'writeEstateNote',
93
+ payload: {'author': _subject, 'body': 'written offline', 'writtenAt': '2026-07-14T00:00:01Z'},
94
+ actor: _subject,
95
+ );
96
+
97
+ // 6 — the UI-facing query returns the write IMMEDIATELY (local projection; nothing awaited sync).
98
+ final localRows = await estate.query('notesByAuthor', {'author': _subject});
99
+ expect(localRows, hasLength(1));
100
+ expect((localRows.single['data'] as Map)['body'], 'written offline');
101
+
102
+ // …and the estate has NO remote custody yet — the partition is real.
103
+ final remoteEstateOffline = await cloud.remoteHead(estateWs);
104
+ expect(remoteEstateOffline.initialized, isFalse,
105
+ reason: 'while partitioned, the locally-born estate must have NO remote custody');
106
+
107
+ // 7+8 — HEAL, then converge parent-first (home's chain lands; its birth offer-effect creates the
108
+ // estate's remote custody; the estate then converges against it).
109
+ cloud.setOnline(true);
110
+ await harness.converge(timeout: const Duration(seconds: 180));
111
+
112
+ // 9 — a FRESH observer: the dev cloud's own remote projection serves the estate write.
113
+ final http = HttpClient();
114
+ try {
115
+ final req = await http.getUrl(Uri.parse(
116
+ '${cloud.origin}/v2/workspaces/$estateWs/notesByAuthor?author=${Uri.encodeQueryComponent(_subject)}'));
117
+ final resp = await req.close();
118
+ final body = jsonDecode(await resp.transform(utf8.decoder).join());
119
+ final rows = ((body as Map)['rows'] as List?) ?? const [];
120
+ expect(rows, hasLength(1),
121
+ reason: 'the remote projection (a fresh observer) serves the offline-written note after converge');
122
+ expect(((rows.single as Map)['data'] as Map)['body'], 'written offline');
123
+ } finally {
124
+ http.close(force: true);
125
+ }
126
+
127
+ // 10 — custody is REAL on both (the dev cloud adopted the chains through the one gate).
128
+ final remoteHome2 = await cloud.remoteHead(home.workspace);
129
+ final remoteEstate2 = await cloud.remoteHead(estateWs);
130
+ expect(remoteHome2.initialized, isTrue);
131
+ expect(remoteHome2.commits, greaterThan(0));
132
+ expect(remoteEstate2.initialized, isTrue,
133
+ reason: 'converge established the estate\'s remote custody (parent birth folded remotely)');
134
+ expect(remoteEstate2.commits, greaterThan(0));
135
+ } finally {
136
+ try { await harness?.dispose(); } catch (_) {}
137
+ await cloud.stop();
138
+ }
139
+ }, timeout: const Timeout(Duration(minutes: 6)));
140
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.89.0",
3
+ "version": "0.90.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",
@@ -1050,7 +1050,7 @@ const NOMOS_TYPES_DEP = "^0.4.1";
1050
1050
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1051
1051
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1052
1052
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1053
- const NOMOS_CLIENT_DEP = "^0.56.0";
1053
+ const NOMOS_CLIENT_DEP = "^0.57.0";
1054
1054
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1055
1055
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1056
1056
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -2777,7 +2777,11 @@ async function main(): Promise<void> {
2777
2777
  unresolved = `${path.relative(process.cwd(), pkg)}: ${`${err.stdout ?? ""}${err.stderr ?? ""}`.trim().split("\n").slice(-3).join(" ")}`;
2778
2778
  }
2779
2779
  }
2780
- if (!dartMissing && inPackage.length > 0) {
2780
+ // Only analyze when EVERY package's deps resolved: `dart analyze` against an unresolved package reports
2781
+ // every framework symbol (NomosRef, …) as undefined — a meaningless flood, not a real defect. An
2782
+ // unresolved dep is already surfaced as UNVERIFIED below (e.g. a fixture pinning an as-yet-unpublished
2783
+ // nomos_client — it verifies for real the moment that version lands).
2784
+ if (!dartMissing && unresolved === undefined && inPackage.length > 0) {
2781
2785
  try {
2782
2786
  execFileSync("dart", ["analyze", "--no-fatal-warnings", ...inPackage], { stdio: "pipe", encoding: "utf8" });
2783
2787
  } catch (e) {