@githolon/dsl 0.93.0 → 0.94.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.
Files changed (34) hide show
  1. package/dart/lib/src/dev_cloud.dart +1 -1
  2. package/dart/lib/src/node_runner_transport.dart +5 -5
  3. package/dart/lib/src/test_harness.dart +16 -16
  4. package/dart/lib/src/webview_bridge.dart +15 -1
  5. package/dart/pubspec.lock +1 -1
  6. package/dart/test/acquire_session_credentials_test.dart +1 -1
  7. package/dart/test/adv_realm_interference_test.dart +2 -2
  8. package/dart/test/cloud_bound_topology_test.dart +2 -2
  9. package/dart/test/cluster_attested_test.dart +1 -1
  10. package/dart/test/cluster_births_test.dart +1 -1
  11. package/dart/test/cluster_device_keyed_acceptance_test.dart +1 -1
  12. package/dart/test/cluster_local_autosync_test.dart +1 -1
  13. package/dart/test/cluster_scale_test.dart +1 -1
  14. package/dart/test/co2_acceptance_test.dart +1 -1
  15. package/dart/test/dlq_session_surface_test.dart +1 -1
  16. package/dart/test/first_app_gauntlet_test.dart +1 -1
  17. package/dart/test/local_sync_inert_test.dart +1 -1
  18. package/dart/test/node_runner_transport_test.dart +3 -3
  19. package/dart/test/parent_signer_recovery_test.dart +1 -1
  20. package/dart/test/sealing_client_test.dart +1 -1
  21. package/dart/test/session_bound_test.dart +1 -1
  22. package/dart/test/test_harness_test.dart +1 -1
  23. package/dart/test/vo_read_typing_test.dart +1 -1
  24. package/dart/test/watch_after_offer_stall_test.dart +1 -1
  25. package/dart/test/watch_surfaces_test.dart +1 -1
  26. package/dart_types/lib/src/errors.dart +27 -0
  27. package/package.json +1 -1
  28. package/src/build_package.ts +13 -1
  29. package/src/codegen_dart_client.ts +10 -0
  30. package/src/codegen_ts.ts +17 -4
  31. package/src/compile_package_main.ts +25 -0
  32. package/src/framework/birth_meter.ts +1 -1
  33. package/src/framework/home.ts +1 -1
  34. package/src/framework/workspaces.ts +1 -1
@@ -84,7 +84,7 @@ class NomosTestCloud {
84
84
 
85
85
  /// Start the test cloud: spawn `node <serverEntry> --port <n> [--ephemeral] [--data <dir>]`, wait until its
86
86
  /// `/health` answers, then bring up the partition proxy in front of it. [serverEntry] is the dev-server
87
- /// entry (`cloud/dev-server/server.mjs` in-repo, or `dist/dev-server.mjs` from a published `githolon`) — the
87
+ /// entry (`cloud/dev-server/server.ts` in-repo, or `dist/dev-server.mjs` from a published `githolon`) — the
88
88
  /// caller resolves it (the same seam the offline harness uses for its install helper). [node] defaults to
89
89
  /// `node` on PATH.
90
90
  static Future<NomosTestCloud> start({
@@ -1,5 +1,5 @@
1
1
  // THE HEADLESS (NODE) TRANSPORT — a [BridgeTransport] over the `@githolon/client` node runner
2
- // (`runner/node-entry.mjs`), spawned as a plain child process speaking the ONE bridge protocol as
2
+ // (`runner/node-entry.ts`), spawned as a plain child process speaking the ONE bridge protocol as
3
3
  // newline-delimited JSON on stdio. This is what lets `dart test` / `flutter test` / CI drive the REAL
4
4
  // githolon (the byte-identical wasm, the real ledger, the real gate) through [NomosBridge] and the
5
5
  // generated typed clients — no Flutter plugins, no WebView, no assets. The only environment requirement
@@ -62,8 +62,8 @@ class NodeRunnerTransport implements BridgeTransport {
62
62
  /// [NomosBridge] and `await bridge.ready` (the runner posts `{event:'ready'}`
63
63
  /// on boot), then `connect(...)` as any other transport.
64
64
  ///
65
- /// [entry] — path to `node-entry.mjs`. When omitted it is resolved with
66
- /// `require.resolve('@githolon/client/runner/node-entry.mjs')` from
65
+ /// [entry] — path to `node-entry.ts`. When omitted it is resolved with
66
+ /// `require.resolve('@githolon/client/runner/node-entry.ts')` from
67
67
  /// [resolveFrom] (default: the current directory), i.e. the consuming app's
68
68
  /// own `node_modules`.
69
69
  static Future<NodeRunnerTransport> start({
@@ -87,13 +87,13 @@ class NodeRunnerTransport implements BridgeTransport {
87
87
  static Future<String> _resolveEntry(String node, String from) async {
88
88
  final r = await Process.run(
89
89
  node,
90
- ['-p', "require.resolve('@githolon/client/runner/node-entry.mjs')"],
90
+ ['-p', "require.resolve('@githolon/client/runner/node-entry.ts')"],
91
91
  workingDirectory: from,
92
92
  );
93
93
  final out = (r.stdout as String).trim();
94
94
  if (r.exitCode != 0 || out.isEmpty) {
95
95
  throw StateError(
96
- 'cannot resolve @githolon/client/runner/node-entry.mjs from $from — '
96
+ 'cannot resolve @githolon/client/runner/node-entry.ts from $from — '
97
97
  'is @githolon/client installed (npm install)? stderr: ${r.stderr}');
98
98
  }
99
99
  return out;
@@ -16,18 +16,18 @@
16
16
  // ```
17
17
  //
18
18
  // HOW IT STAYS OFFLINE. Two node children, both talking only to the local disk:
19
- // 1. `offline-install.mjs` runs the PROVEN offline install lane (`githolon proof`'s adapter engine) —
19
+ // 1. `offline-install.ts` runs the PROVEN offline install lane (`githolon proof`'s adapter engine) —
20
20
  // genesis → framework controller → each tenant package's `installDomain`, folded through the ONE gate —
21
21
  // and serializes the workspace tree to a snapshot in a fresh temp dir.
22
- // 2. the node runner (`node-entry.mjs`) boots in `NOMOS_OFFLINE=1` mode: it serves the runtime wasm from the
22
+ // 2. the node runner (`node-entry.ts`) boots in `NOMOS_OFFLINE=1` mode: it serves the runtime wasm from the
23
23
  // local cache (`~/.holon/runtime`, filled by any warm `githolon` run) and boots the holon from that
24
24
  // snapshot (`restoreFrom`). Every other URL is REFUSED — a stray fetch fails loudly, never reaches the net.
25
25
  //
26
26
  // PREREQUISITES (documented so a failure is legible): `node` on PATH; a resolvable
27
- // `@githolon/client/runner/node-entry.mjs`; and a warm `~/.holon/runtime` cache (holon.wasm + packages.json —
27
+ // `@githolon/client/runner/node-entry.ts`; and a warm `~/.holon/runtime` cache (holon.wasm + packages.json —
28
28
  // one `githolon` run fills it). All local; no network at test time.
29
29
  //
30
- // PACKAGING NOTE: `offline-install.mjs` imports the adapter engine, which today lives beside the in-repo
30
+ // PACKAGING NOTE: `offline-install.ts` imports the adapter engine, which today lives beside the in-repo
31
31
  // runner. For a PUBLISHED harness this install belongs behind a `githolon` subcommand (the CLI already
32
32
  // bundles the engine + this exact sequence in `proof_offline`); [installHelper] is the one seam to repoint.
33
33
  import 'dart:convert';
@@ -246,10 +246,10 @@ class NomosTestHarness {
246
246
  /// framework/platform law before a domain that depends on it).
247
247
  /// [workspace] — the local workspace name (default `test-ws`; law-only, never leaves the machine).
248
248
  /// [runtimeDir] — the runtime cache (default `~/.holon/runtime`); must hold `holon.wasm` + `packages.json`.
249
- /// [nodeEntry] — path to `node-entry.mjs`; when omitted it is resolved from [resolveFrom] via
250
- /// `require.resolve('@githolon/client/runner/node-entry.mjs')`.
249
+ /// [nodeEntry] — path to `node-entry.ts`; when omitted it is resolved from [resolveFrom] via
250
+ /// `require.resolve('@githolon/client/runner/node-entry.ts')`.
251
251
  /// [installHelper] — the offline law-install lane. An EXISTING `.mjs` file is run as a node script (the
252
- /// in-repo `offline-install.mjs`, the default when it exists beside [nodeEntry]); anything else is a
252
+ /// in-repo `offline-install.ts`, the default when it exists beside [nodeEntry]); anything else is a
253
253
  /// githolon COMMAND LINE whose `harness-install` subcommand is driven (the default `npx githolon` when the
254
254
  /// in-repo helper is absent — a published tenant needs nothing but `githolon`). E.g. force the subcommand
255
255
  /// with `installHelper: 'node /abs/cli/dist/cli.mjs'`.
@@ -280,14 +280,14 @@ class NomosTestHarness {
280
280
  final entry = nodeEntry ??
281
281
  await _resolveEntry(node, resolveFrom ?? Directory.current.path);
282
282
  // THE INSTALL LANE. [installHelper] is the one seam (PACKAGING NOTE above): either the in-repo
283
- // `offline-install.mjs` node script, OR the published `githolon harness-install` subcommand (the CLI
283
+ // `offline-install.ts` node script, OR the published `githolon harness-install` subcommand (the CLI
284
284
  // bundles the engine + this exact install sequence, so a published tenant needs nothing but `githolon`).
285
285
  // • an EXISTING `.mjs` file ⇒ run it as a node script (the in-repo default when present);
286
286
  // • anything else ⇒ treat it as a githolon COMMAND LINE and drive `harness-install`
287
287
  // (default `npx githolon` when the in-repo helper is absent; the test forces this lane by passing
288
288
  // e.g. `installHelper: 'node /abs/cli/dist/cli.mjs'`).
289
289
  final resolvedHelper =
290
- installHelper ?? '${File(entry).parent.path}/offline-install.mjs';
290
+ installHelper ?? '${File(entry).parent.path}/offline-install.ts';
291
291
  final useInRepoScript =
292
292
  resolvedHelper.endsWith('.mjs') && File(resolvedHelper).existsSync();
293
293
  final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
@@ -303,7 +303,7 @@ class NomosTestHarness {
303
303
  final info = await _phased(
304
304
  phase: '1/4: offline law install (${deployJsonPaths.length} package(s))',
305
305
  workspace: workspace,
306
- awaiting: useInRepoScript ? 'offline-install.mjs' : 'githolon harness-install',
306
+ awaiting: useInRepoScript ? 'offline-install.ts' : 'githolon harness-install',
307
307
  timeout: phaseTimeout ?? _kInstallDeadline,
308
308
  onDiagnostic: onDiagnostic,
309
309
  likelyCauses: const [
@@ -465,7 +465,7 @@ class NomosTestHarness {
465
465
  final entry = nodeEntry ??
466
466
  await _resolveEntry(node, resolveFrom ?? Directory.current.path);
467
467
  final resolvedHelper =
468
- installHelper ?? '${File(entry).parent.path}/offline-install.mjs';
468
+ installHelper ?? '${File(entry).parent.path}/offline-install.ts';
469
469
  final useInRepoScript =
470
470
  resolvedHelper.endsWith('.mjs') && File(resolvedHelper).existsSync();
471
471
  final githolonCmd = installHelper == null ? 'npx githolon' : resolvedHelper;
@@ -483,7 +483,7 @@ class NomosTestHarness {
483
483
  final info = await _phased(
484
484
  phase: '1: offline law install ${++_i}/${workspaces.length}',
485
485
  workspace: e.key,
486
- awaiting: useInRepoScript ? 'offline-install.mjs' : 'githolon harness-install',
486
+ awaiting: useInRepoScript ? 'offline-install.ts' : 'githolon harness-install',
487
487
  timeout: phaseTimeout ?? _kInstallDeadline,
488
488
  onDiagnostic: onDiagnostic,
489
489
  likelyCauses: const [
@@ -911,7 +911,7 @@ class NomosTestHarness {
911
911
  final info = await _phased(
912
912
  phase: 'seed: offline law install',
913
913
  workspace: workspace,
914
- awaiting: lane.useInRepoScript ? 'offline-install.mjs' : 'githolon harness-install',
914
+ awaiting: lane.useInRepoScript ? 'offline-install.ts' : 'githolon harness-install',
915
915
  timeout: _phaseTimeout ?? _kInstallDeadline,
916
916
  onDiagnostic: _onDiagnostic,
917
917
  run: () => _runInstall(
@@ -1106,7 +1106,7 @@ class NomosTestHarness {
1106
1106
  githolonCmd.trim().split(RegExp(r'\s+')).where((t) => t.isNotEmpty).toList();
1107
1107
  if (tokens.isEmpty) {
1108
1108
  throw NomosTestHarnessException(
1109
- 'no install helper resolved — pass installHelper: (an offline-install.mjs path, or a githolon command)');
1109
+ 'no install helper resolved — pass installHelper: (an offline-install.ts path, or a githolon command)');
1110
1110
  }
1111
1111
  executable = tokens.first;
1112
1112
  args = [...tokens.skip(1), 'harness-install', ...common];
@@ -1143,13 +1143,13 @@ class NomosTestHarness {
1143
1143
  static Future<String> _resolveEntry(String node, String from) async {
1144
1144
  final r = await Process.run(
1145
1145
  node,
1146
- ['-p', "require.resolve('@githolon/client/runner/node-entry.mjs')"],
1146
+ ['-p', "require.resolve('@githolon/client/runner/node-entry.ts')"],
1147
1147
  workingDirectory: from,
1148
1148
  );
1149
1149
  final out = (r.stdout as String).trim();
1150
1150
  if (r.exitCode != 0 || out.isEmpty) {
1151
1151
  throw NomosTestHarnessException(
1152
- 'cannot resolve @githolon/client/runner/node-entry.mjs from $from — '
1152
+ 'cannot resolve @githolon/client/runner/node-entry.ts from $from — '
1153
1153
  'is @githolon/client installed (npm install)? Pass nodeEntry: to override. stderr: ${r.stderr}');
1154
1154
  }
1155
1155
  return out;
@@ -48,7 +48,7 @@ const int kWebviewBridgeVersion = 1;
48
48
  /// pre-stamp bundle, which reports nothing) is a stale baked asset and fails loudly at boot.
49
49
  /// MUST move with the paired @githolon/client release — a release-hygiene test pairs it with the
50
50
  /// runner stamp (the pubspec-pins pattern).
51
- const String kExpectedRunnerBundleVersion = '0.80.0';
51
+ const String kExpectedRunnerBundleVersion = '0.93.0';
52
52
 
53
53
  /// A STALE BAKED RUNNER BUNDLE: the app's built assets carry an older nomos-runner.js than this
54
54
  /// nomos_client release ships — Flutter's build cache kept it across a pub upgrade. Thrown from
@@ -1592,6 +1592,20 @@ class NomosBridge {
1592
1592
  /// [attestorSecret] is the source's base64 read-attestor secret (from [authorKeygen]); when omitted the
1593
1593
  /// source session's connect-time `authorSecret` signs. [nowMs] stamps the freshness clock (defaults to the
1594
1594
  /// source's own clock).
1595
+ /// RELEASE COHERENCE — assert the connected cloud's kernel supports every capability this law needs (the
1596
+ /// generated client passes its baked `requiresKernel`, e.g. `['compoundKeys']` for a compound-key query).
1597
+ /// Throws a typed [KernelCoherenceError] on a skew (a stale/mismatched cloud kernel) with the named reason,
1598
+ /// instead of an opaque deep-fold refusal. A best-effort no-op when the cloud advertises no capabilities
1599
+ /// (older worker / offline). Called automatically by the generated client at bind; safe to call directly.
1600
+ Future<void> assertKernelCoherence(List<String> requiresKernel,
1601
+ {String? lawHash, String? session}) async {
1602
+ await _call('assertKernelCoherence', {
1603
+ 'requiresKernel': requiresKernel,
1604
+ if (lawHash != null) 'lawHash': lawHash,
1605
+ if (session != null) 'session': session,
1606
+ });
1607
+ }
1608
+
1595
1609
  Future<Map<String, Object?>> attestedRead({
1596
1610
  required String from,
1597
1611
  required String queryId,
package/dart/pubspec.lock CHANGED
@@ -191,7 +191,7 @@ packages:
191
191
  path: "../dart_types"
192
192
  relative: true
193
193
  source: path
194
- version: "0.4.1"
194
+ version: "0.5.0"
195
195
  package_config:
196
196
  dependency: transitive
197
197
  description:
@@ -17,7 +17,7 @@ import 'package:nomos_client/nomos_headless.dart';
17
17
  import 'package:test/test.dart';
18
18
 
19
19
  String get _root => '${Directory.current.path}/../..';
20
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
20
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
21
21
  String get _docsDeploy => '$_root/bench/scale/fixtures/surfaced/build/surfaced.deploy.json';
22
22
  String get _runtimeDir =>
23
23
  Platform.environment['NOMOS_DART_RUNTIME'] ??
@@ -25,8 +25,8 @@ import 'package:nomos_types/nomos_types.dart' show NomosRef;
25
25
  import 'package:test/test.dart';
26
26
 
27
27
  String get _root => '${Directory.current.path}/../..';
28
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
29
- String get _installer => '$_root/cloud/web-client/runner/offline-install.mjs';
28
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
29
+ String get _installer => '$_root/cloud/web-client/runner/offline-install.ts';
30
30
  String get _guestbook => '$_root/examples/guestbook/build/guestbook.deploy.json';
31
31
  String get _surfaced => '$_root/bench/scale/fixtures/surfaced/build/surfaced.deploy.json';
32
32
  String get _attributed => '$_root/bench/scale/fixtures/attributed/build/attributed.deploy.json';
@@ -21,8 +21,8 @@ import 'package:nomos_client/nomos_headless.dart';
21
21
  import 'package:test/test.dart';
22
22
 
23
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';
24
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
25
+ String get _devServer => '$_root/cloud/dev-server/server.ts';
26
26
  String get _deploy =>
27
27
  '$_root/bench/scale/fixtures/cloud-topology/build/cloud-topology.deploy.json';
28
28
  const _subject = 'user:topology-test';
@@ -26,7 +26,7 @@ import 'package:test/test.dart';
26
26
  import '../../../bench/scale/fixtures/catalogue-reader/build/dart/src/generated/estate.client.dart';
27
27
 
28
28
  String get _root => '${Directory.current.path}/../..';
29
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
29
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
30
30
  String get _catDeploy => '$_root/bench/scale/fixtures/catalogue-src/build/cat.deploy.json';
31
31
  String get _estateDeploy => '$_root/bench/scale/fixtures/catalogue-reader/build/estate.deploy.json';
32
32
  String get _estateClientDart =>
@@ -27,7 +27,7 @@ import 'package:nomos_client/nomos_headless.dart';
27
27
  import 'package:test/test.dart';
28
28
 
29
29
  String get _root => '${Directory.current.path}/../..';
30
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
30
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
31
31
  String get _birthDeploy =>
32
32
  '$_root/bench/scale/fixtures/estate-birth/build/estate-birth.deploy.json';
33
33
  String get _runtimeDir => '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
@@ -17,7 +17,7 @@ import 'package:nomos_client/nomos_headless.dart';
17
17
  import 'package:test/test.dart';
18
18
 
19
19
  String get _root => '${Directory.current.path}/../..';
20
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
20
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
21
21
  String get _catDeploy => '$_root/bench/scale/fixtures/catalogue-src/build/cat.deploy.json';
22
22
  String get _estateDeploy => '$_root/bench/scale/fixtures/catalogue-reader/build/estate.deploy.json';
23
23
  String get _runtimeDir => '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
@@ -15,7 +15,7 @@ import 'package:nomos_client/nomos_headless.dart';
15
15
  import 'package:test/test.dart';
16
16
 
17
17
  String get _root => '${Directory.current.path}/../..';
18
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
18
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
19
19
  String get _guestbookDeploy => '$_root/examples/guestbook/build/guestbook.deploy.json';
20
20
  String get _runtimeDir => '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
21
21
 
@@ -15,7 +15,7 @@ import 'package:nomos_client/nomos_headless.dart';
15
15
  import 'package:test/test.dart';
16
16
 
17
17
  String get _root => '${Directory.current.path}/../..';
18
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
18
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
19
19
  String get _guestbookDeploy => '$_root/examples/guestbook/build/guestbook.deploy.json';
20
20
  String get _runtimeDir => '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
21
21
 
@@ -13,7 +13,7 @@ import 'package:test/test.dart';
13
13
  import '../../../bench/scale/fixtures/co2-acceptance/build/dart/src/generated/estate.client.dart';
14
14
 
15
15
  String get _root => '${Directory.current.path}/../..';
16
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
16
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
17
17
  String get _deployJson =>
18
18
  '$_root/bench/scale/fixtures/co2-acceptance/build/co2-acceptance.deploy.json';
19
19
  String get _runtimeDir => '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
@@ -19,7 +19,7 @@ import 'package:nomos_client/nomos_headless.dart';
19
19
  import 'package:test/test.dart';
20
20
 
21
21
  String get _root => '${Directory.current.path}/../..';
22
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
22
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
23
23
  String get _geoDeploy => '$_root/bench/scale/fixtures/estate-geo/build/estate-geo.deploy.json';
24
24
  String get _docsDeploy => '$_root/bench/scale/fixtures/surfaced/build/surfaced.deploy.json';
25
25
  String get _runtimeDir =>
@@ -24,7 +24,7 @@ import '../../../bench/scale/fixtures/estate-birth/build/dart/src/generated/esta
24
24
  show EstateClient, SeedEstateWorkspacePayload;
25
25
 
26
26
  String get _root => '${Directory.current.path}/../..';
27
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
27
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
28
28
  String get _homeDeploy =>
29
29
  '$_root/bench/scale/fixtures/estate-birth/build/estate-birth.deploy.json';
30
30
  String get _docsDeploy => '$_root/bench/scale/fixtures/surfaced/build/surfaced.deploy.json';
@@ -13,7 +13,7 @@ import 'package:test/test.dart';
13
13
  import '../../../bench/scale/fixtures/estate-birth/build/dart/src/generated/home.client.dart';
14
14
 
15
15
  String get _root => '${Directory.current.path}/../..';
16
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
16
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
17
17
  String get _deployJson =>
18
18
  '$_root/bench/scale/fixtures/estate-birth/build/estate-birth.deploy.json';
19
19
  String get _runtimeDir =>
@@ -1,4 +1,4 @@
1
- // The headless transport against the REAL node runner (`cloud/web-client/runner/node-entry.mjs`):
1
+ // The headless transport against the REAL node runner (`cloud/web-client/runner/node-entry.ts`):
2
2
  // protocol round-trip through NomosBridge without touching the cloud — ready handshake, typed refusal
3
3
  // on a pre-connect op, unknown-op error. The live connect path is covered by the env-gated smoke below.
4
4
  @TestOn('vm')
@@ -13,9 +13,9 @@ import 'package:test/test.dart';
13
13
  String _repoEntry() {
14
14
  // In-repo the runner lives beside us; consumers resolve it from node_modules instead.
15
15
  final here = Directory.current.path;
16
- final p = '$here/../../cloud/web-client/runner/node-entry.mjs';
16
+ final p = '$here/../../cloud/web-client/runner/node-entry.ts';
17
17
  if (!File(p).existsSync()) {
18
- throw StateError('node-entry.mjs not found at $p');
18
+ throw StateError('node-entry.ts not found at $p');
19
19
  }
20
20
  return p;
21
21
  }
@@ -22,7 +22,7 @@ import 'package:nomos_types/nomos_types.dart'
22
22
  import 'package:test/test.dart';
23
23
 
24
24
  String get _root => '${Directory.current.path}/../..';
25
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
25
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
26
26
  String get _deploy =>
27
27
  '$_root/bench/scale/fixtures/estate-birth-signed/build/estate-birth-signed.deploy.json';
28
28
  String get _runtimeDir => '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
@@ -16,7 +16,7 @@ import 'package:nomos_client/nomos_headless.dart';
16
16
  import 'package:test/test.dart';
17
17
 
18
18
  String get _root => '${Directory.current.path}/../..';
19
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
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.deploy.json';
22
22
  String get _lockfile => '$_fixture/nomos.stable-ids.json';
@@ -27,7 +27,7 @@ import '../../../bench/scale/fixtures/estate-birth/build/dart/src/generated/esta
27
27
  show EstateClient, SeedEstateWorkspacePayload;
28
28
 
29
29
  String get _root => '${Directory.current.path}/../..';
30
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
30
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
31
31
  String get _deployJson =>
32
32
  '$_root/bench/scale/fixtures/estate-birth/build/estate-birth.deploy.json';
33
33
  String get _otherDeployJson =>
@@ -15,7 +15,7 @@ import 'package:nomos_client/nomos_headless.dart';
15
15
  import 'package:test/test.dart';
16
16
 
17
17
  String get _repoEntry =>
18
- '${Directory.current.path}/../../cloud/web-client/runner/node-entry.mjs';
18
+ '${Directory.current.path}/../../cloud/web-client/runner/node-entry.ts';
19
19
  String get _guestbookDeployJson =>
20
20
  '${Directory.current.path}/../../examples/guestbook/build/guestbook.deploy.json';
21
21
  String get _runtimeDir =>
@@ -19,7 +19,7 @@ import 'package:test/test.dart';
19
19
  import '../../../bench/scale/fixtures/vo-read-typing/build/dart/src/generated/estate.dart';
20
20
 
21
21
  String get _root => '${Directory.current.path}/../..';
22
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
22
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
23
23
  String get _deployJson =>
24
24
  '$_root/bench/scale/fixtures/vo-read-typing/build/vo-read-typing.deploy.json';
25
25
  String get _runtimeDir => '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
@@ -12,7 +12,7 @@ import 'package:nomos_client/nomos_headless.dart';
12
12
  import 'package:test/test.dart';
13
13
 
14
14
  String get _root => '${Directory.current.path}/../..';
15
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
15
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
16
16
  String get _gbDeploy => '$_root/examples/guestbook/build/guestbook.deploy.json';
17
17
  String get _runtimeDir =>
18
18
  Platform.environment['NOMOS_DART_RUNTIME'] ??
@@ -20,7 +20,7 @@ import 'package:test/test.dart';
20
20
  import '../../../bench/scale/fixtures/co2-acceptance/build/dart/src/generated/estate.client.dart';
21
21
 
22
22
  String get _root => '${Directory.current.path}/../..';
23
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.mjs';
23
+ String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
24
24
  String get _geoDeploy =>
25
25
  '$_root/bench/scale/fixtures/co2-acceptance/build/co2-acceptance.deploy.json';
26
26
  String get _actsDeploy =>
@@ -29,6 +29,9 @@ NomosBridgeError nomosErrorFromInfo(NomosErrorInfo info) {
29
29
  if (info.code == 'nomos.attested_read.source_not_mounted') {
30
30
  return SourceSessionNotMounted.fromInfo(info);
31
31
  }
32
+ if (info.code == 'kernel-capability-skew') {
33
+ return KernelCoherenceError.fromInfo(info);
34
+ }
32
35
  return NomosBridgeError(info.message, info: info);
33
36
  }
34
37
 
@@ -480,6 +483,30 @@ class SourceSessionNotMounted extends NomosBridgeError {
480
483
  '${queryId != null ? ', query: $queryId' : ''}, recommendedAction: $recommendedAction): $message';
481
484
  }
482
485
 
486
+ /// KERNEL COHERENCE SKEW (`kernel-capability-skew`) — the cloud this client connected to runs a kernel that
487
+ /// does NOT support a capability this law requires (e.g. a compound-key query `.key(a, b)` needs
488
+ /// `compoundKeys`, and the cloud's kernel is a STALE build that predates the compound-key read router). The
489
+ /// generated client asserts this at bind (from its baked `requiresKernel`) against the cloud's advertised
490
+ /// `/v1/runtime/capabilities`, so a stale/mismatched kernel is caught UP FRONT with a named reason — instead
491
+ /// of the opaque "the read router supports exactly one" refusal deep in a law fold. Remedy: point at a cloud
492
+ /// whose kernel is coherent with this release, or upgrade the cloud's kernel.
493
+ class KernelCoherenceError extends NomosBridgeError {
494
+ /// The stable machine code — always `kernel-capability-skew`.
495
+ final String code;
496
+
497
+ KernelCoherenceError({
498
+ this.code = 'kernel-capability-skew',
499
+ required String message,
500
+ NomosErrorInfo? info,
501
+ }) : super(message, info: info);
502
+
503
+ factory KernelCoherenceError.fromInfo(NomosErrorInfo info) =>
504
+ KernelCoherenceError(code: info.code, message: info.message, info: info);
505
+
506
+ @override
507
+ String toString() => 'KernelCoherenceError: $message';
508
+ }
509
+
483
510
  /// The custody phase a [CustodyError] was raised in.
484
511
  enum CustodyPhase { fold, author, fetch, unknown }
485
512
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.93.0",
3
+ "version": "0.94.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",
@@ -237,7 +237,19 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
237
237
  // byte-identical; a domain that DOES could not have deployed before without the author hand-writing this.
238
238
  const autoDeclaredEnsures: string[] = [];
239
239
  for (const ty of discoverUnsanctionedEnsures(directives, spec.domain ?? spec.name)) {
240
- const agg = byId.get(ty);
240
+ let agg = byId.get(ty);
241
+ // SELF-CONTAINED FRAMEWORK RELATIONS: `seedHomeOwner`/`writeTuple` (and any `relateSubject`-style seat)
242
+ // upsert the canonical framework `RelationTuple` aggregate at a DETERMINISTIC id. A tenant that re-exports
243
+ // ONLY the seating directive (e.g. `seedHomeOwner`) — without also re-exporting `RelationTuple` + a
244
+ // `.ensures(RelationTuple)` directive — carries no RelationTuple handle, so the id-mint gate would refuse
245
+ // its reltuple id. RelationTuple is a UNIVERSAL framework aggregate (one canonical shape), so supply its
246
+ // handle here and carry it, making the seat self-contained. Narrow + regression-free: this fires ONLY when
247
+ // a domain writes RelationTuple WITHOUT carrying it — which fails today — so no already-compiling domain
248
+ // (which must already carry it) changes. Any OTHER uncarried type stays deferred to the runtime gate.
249
+ if (agg === undefined && ty === RelationTuple.id) {
250
+ agg = RelationTuple;
251
+ byId.set(RelationTuple.id, RelationTuple);
252
+ }
241
253
  // Only sanction a type whose handle this domain actually carries (its schema must be present for the
242
254
  // upsert to fold); a probe pointing at a foreign/unknown type is left to the runtime gate (no schema).
243
255
  if (agg === undefined) continue;
@@ -33,6 +33,7 @@ import type { Field } from "./fields.js";
33
33
  import type { Directive } from "./directive.js";
34
34
  import type { QueryDecl } from "./query.js";
35
35
  import type { DomainModule } from "./codegen_dart.js";
36
+ import { requiredKernelFeatures } from "./kernel_capabilities.js";
36
37
  import {
37
38
  cap,
38
39
  readModelClassName,
@@ -170,6 +171,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
170
171
  const domain = mod.domain ?? mod.name;
171
172
  const className = dartClientClassName(domain);
172
173
  const hashConst = dartHashConstName(mod.name);
174
+ const requiresKernelFeats = requiredKernelFeatures([mod]);
173
175
  const supportImport =
174
176
  opts.supportImport === "package"
175
177
  ? "package:nomos_client/nomos_client.dart"
@@ -824,6 +826,10 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
824
826
  ` /// THE STATIC INTERFACE DESCRIPTOR (session_bound_clients.md asks 5/9): what this client EXPECTS`,
825
827
  ` /// of the workspace it binds to — the domain key, the law hash it was generated against, and the`,
826
828
  ` /// structural interface (#72). Consumed by [bind] and by \`session.assertCompatible(${className}.interface)\`.`,
829
+ ` /// The kernel capabilities this law REQUIRES (release coherence): a compound-key query needs`,
830
+ ` /// 'compoundKeys', etc. [bind] asserts the connected cloud's kernel advertises these.`,
831
+ ` static const List<String> requiresKernel = [${requiresKernelFeats.map((f) => dartStr(f)).join(", ")}];`,
832
+ ``,
827
833
  ` static const NomosClientInterface interface = NomosClientInterface(`,
828
834
  ` domain: ${dartStr(domain)},`,
829
835
  ` expectedDomainHash: ${hashConst},`,
@@ -841,6 +847,10 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
841
847
  ` /// first dispatch.`,
842
848
  ` static Future<${className}> bind(NomosSession session, {String? actor${encPresent ? ", NomosKeyStore? keyStore" : ""}}) async {`,
843
849
  ` await session.assertCompatible(interface);`,
850
+ ` // RELEASE COHERENCE: the cloud's kernel must support what this law needs (e.g. a compound-key query`,
851
+ ` // needs 'compoundKeys'). Throws a typed KernelCoherenceError on a stale/mismatched cloud kernel,`,
852
+ ` // up front — not an opaque deep-fold refusal. Best-effort (no-op if the cloud advertises no caps).`,
853
+ ` await session.assertKernelCoherence(requiresKernel, lawHash: ${hashConst});`,
844
854
  ` return ${className}(session, actor: actor${encPresent ? ", keyStore: keyStore" : ""}).._preflighted = true;`,
845
855
  ` }`,
846
856
  ``,
package/src/codegen_ts.ts CHANGED
@@ -66,6 +66,19 @@ const tsGroupParam = (by: string) => {
66
66
  return parts.length <= 1 ? by : parts[0]! + parts.slice(1).map((p) => p[0]!.toUpperCase() + p.slice(1)).join("");
67
67
  };
68
68
 
69
+ /** An object-literal / interface member KEY. Field LABELS may be dotted structural paths
70
+ * (`spec.domainHash`, `status.phase`) or otherwise non-identifier — emitted bare they'd
71
+ * parse as member access and break the file. A valid JS identifier is emitted verbatim;
72
+ * anything else is quoted as a string-literal key (same runtime property, valid TS). */
73
+ const tsKey = (name: string): string =>
74
+ /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
75
+
76
+ /** OPTIONAL-CHAIN member access onto a value keyed by a possibly-dotted field label,
77
+ * meant to follow a `?` (e.g. `x?${tsOptAccess(f)}`): `.foo` for an identifier,
78
+ * `.["foo.bar"]` otherwise (bracket access preserves the single property). */
79
+ const tsOptAccess = (name: string): string =>
80
+ /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? `.${name}` : `.[${JSON.stringify(name)}]`;
81
+
69
82
  // ── NAME NORMALIZATION (no naming convention to learn) ────────────────────────────
70
83
  // `nomos.package.mjs` names and domain keys may be ANY string — kebab-case,
71
84
  // snake_case, space-separated, PascalCase. Artifact FILENAMES keep the raw name
@@ -1486,7 +1499,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1486
1499
  for (const [, c] of [...contractByIdTs].sort(([a], [b]) => a.localeCompare(b))) {
1487
1500
  out.push(`export interface ${c.id}Contract {`);
1488
1501
  for (const [name, field] of Object.entries(c.fields)) {
1489
- out.push(` ${name}?: ${tsTypeOfField(field as Field)};`);
1502
+ out.push(` ${tsKey(name)}?: ${tsTypeOfField(field as Field)};`);
1490
1503
  }
1491
1504
  out.push(`}`, ``);
1492
1505
  }
@@ -1525,14 +1538,14 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1525
1538
  const required = (field as Field).isFromBirth === true;
1526
1539
  const doc = nullDocLine(field);
1527
1540
  if (doc !== undefined) out.push(` /** ${doc} */`);
1528
- out.push(` ${name}${required ? "" : "?"}: ${tsTypeOfField(field as Field)};`);
1541
+ out.push(` ${tsKey(name)}${required ? "" : "?"}: ${tsTypeOfField(field as Field)};`);
1529
1542
  const fr = (field as { foreignRef?: { workspaceField: string; targetAggregateId: string } }).foreignRef;
1530
1543
  if (fr !== undefined) foreignRefFields.push({ agg: typeId, field: name, workspaceField: fr.workspaceField, target: fr.targetAggregateId });
1531
1544
  }
1532
1545
  if (attributed.length > 0) {
1533
1546
  out.push(
1534
1547
  ` /** attributed-read provenance (the reserved per-row map; prefer the generated accessors) */`,
1535
- ` "__prov"?: { ${attributed.map((f) => `${f}?: FieldProvenance`).join("; ")} };`,
1548
+ ` "__prov"?: { ${attributed.map((f) => `${tsKey(f)}?: FieldProvenance`).join("; ")} };`,
1536
1549
  );
1537
1550
  }
1538
1551
  for (const extra of extrasByType.get(typeId) ?? []) out.push(extra);
@@ -1545,7 +1558,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
1545
1558
  ` * unknown-author fields omitted). */`,
1546
1559
  `export const ${ns}Provenance = {`,
1547
1560
  ...attributed.map(
1548
- (f) => ` ${f}: (data: ${typeId}Data): FieldProvenance | undefined => data.__prov?.${f},`,
1561
+ (f) => ` ${tsKey(f)}: (data: ${typeId}Data): FieldProvenance | undefined => data.__prov?${tsOptAccess(f)},`,
1549
1562
  ),
1550
1563
  ` lastAuthors: (data: ${typeId}Data): Record<string, string> => {`,
1551
1564
  ` const out: Record<string, string> = {};`,
@@ -2781,9 +2781,34 @@ async function main(): Promise<void> {
2781
2781
  // files would false-fail every vendored fixture. Analyze the in-package set; note the loose remainder.
2782
2782
  const inPackage = emittedDart.filter((f) => pkgDirOf(f) !== undefined);
2783
2783
  const pkgDirs = [...new Set(inPackage.map(pkgDirOf).filter((d): d is string => d !== undefined))];
2784
+ // DEV DOGFOOD LINK: when this compiler is running FROM SOURCE (a nomos2 checkout, NOT an installed
2785
+ // node_modules copy), the emitted layered packages pin the PUBLISHED nomos_client/nomos_types — which
2786
+ // necessarily LAG the local source the compiler was built alongside (tooling+client move in lockstep,
2787
+ // CLAUDE.md). So the analyze gate must resolve against the LOCAL Dart source, else it judges freshly-
2788
+ // emitted client code against a stale hosted runtime (e.g. a method the current codegen calls that the
2789
+ // last publish predates). We write a `pubspec_overrides.yaml` pointing nomos_client/nomos_types at the
2790
+ // repo's own `dsl/dart` / `dsl/dart_types`. An INSTALLED tenant (`npx githolon`, compiler under
2791
+ // node_modules) gets NO override — it verifies against exactly the hosted runtime it will ship against.
2792
+ const localDart = (() => {
2793
+ const selfDir = path.dirname(fileURLToPath(import.meta.url));
2794
+ if (selfDir.includes(`${path.sep}node_modules${path.sep}`)) return undefined;
2795
+ const client = path.resolve(selfDir, "..", "dart");
2796
+ const types = path.resolve(selfDir, "..", "dart_types");
2797
+ if (existsSync(path.join(client, "pubspec.yaml")) && existsSync(path.join(types, "pubspec.yaml"))) {
2798
+ return { client, types };
2799
+ }
2800
+ return undefined;
2801
+ })();
2784
2802
  let dartMissing = false;
2785
2803
  let unresolved: string | undefined;
2786
2804
  for (const pkg of pkgDirs) {
2805
+ if (localDart) {
2806
+ const spec = readFileSync(path.join(pkg, "pubspec.yaml"), "utf8");
2807
+ const lines = ["dependency_overrides:"];
2808
+ if (/(^|\n)\s+nomos_client\s*:/.test(spec)) lines.push(` nomos_client:`, ` path: ${localDart.client}`);
2809
+ if (/(^|\n)\s+nomos_types\s*:/.test(spec)) lines.push(` nomos_types:`, ` path: ${localDart.types}`);
2810
+ if (lines.length > 1) writeFileSync(path.join(pkg, "pubspec_overrides.yaml"), lines.join("\n") + "\n", "utf8");
2811
+ }
2787
2812
  try {
2788
2813
  execFileSync("dart", ["pub", "get"], { cwd: pkg, stdio: "pipe", encoding: "utf8" });
2789
2814
  } catch (e) {
@@ -15,7 +15,7 @@
15
15
  * - {@link seedBirthPolicy} — THE GENESIS SEED (A3, ungated like `seedGenesis`): the step a
16
16
  * parent's birth recipe threads (`buildGenesisChain`'s `descendantPool`) so the child is BORN
17
17
  * BOUNDED — the pool chains to root by induction (root bounds platforms, platforms bound
18
- * homes, homes bound estates). Control-plane protected exactly like `seedGenesis`: the open
18
+ * homes, homes bound projects). Control-plane protected exactly like `seedGenesis`: the open
19
19
  * session lane structurally refuses control-plane intents, and post-genesis writes on a
20
20
  * warranted workspace require signed authorship.
21
21
  *
@@ -84,7 +84,7 @@ export const HomeOwner = aggregate("HomeOwner", {
84
84
  * ownership RECORD; this relation is the AUTHORIZATION spine a tenant can compute from. `seedHomeOwner` seats
85
85
  * `(home:self, homeOwner, user:<owner>)` ATOMICALLY inside the same child genesis (before the warrant closes
86
86
  * bootstrap), so a canonical `birthHome` yields a home whose owner passes role gates from intent 0 — a tenant
87
- * declares `estateCreator = computed(homeOwner)` WITHOUT copying birth mechanics. Self-bound (object `home:
87
+ * declares `projectCreator = computed(homeOwner)` WITHOUT copying birth mechanics. Self-bound (object `home:
88
88
  * self`, subject the owner) — the {@link homeNameBindsOwner} squat invariant already proves the owner is the
89
89
  * rightful one, so no separate grant is needed.
90
90
  */
@@ -582,7 +582,7 @@ export function buildGenesisChain(p: {
582
582
  // child is BORN BOUNDED — a `seedBirthPolicy` genesis step writes the child's own BirthPolicy singleton
583
583
  // (its default birth allowance) BEFORE the warrant lands, exactly like every other genesis seed (the
584
584
  // still-warrant-free prefix runs unsigned). Root bounds platforms (this pool), platforms bound homes,
585
- // homes bound estates — the tree meters by induction, with zero cross-workspace reads on any gate.
585
+ // homes bound projects — the tree meters by induction, with zero cross-workspace reads on any gate.
586
586
  // OPT-IN (deliberate): the step folds through the CHILD'S OWN gate under the law hash the order names,
587
587
  // so it may only be threaded when that law carries `seedBirthPolicy` (a law compiled under this DSL) —
588
588
  // an unconditional default would refuse the genesis of every birth pinning an OLDER law hash. V1