@githolon/dsl 0.92.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.
- package/dart/lib/src/dev_cloud.dart +1 -1
- package/dart/lib/src/node_runner_transport.dart +5 -5
- package/dart/lib/src/test_harness.dart +16 -16
- package/dart/lib/src/webview_bridge.dart +15 -1
- package/dart/pubspec.lock +1 -1
- package/dart/test/acquire_session_credentials_test.dart +1 -1
- package/dart/test/adv_realm_interference_test.dart +2 -2
- package/dart/test/cloud_bound_topology_test.dart +2 -2
- package/dart/test/cluster_attested_test.dart +1 -1
- package/dart/test/cluster_births_test.dart +1 -1
- package/dart/test/cluster_device_keyed_acceptance_test.dart +1 -1
- package/dart/test/cluster_local_autosync_test.dart +1 -1
- package/dart/test/cluster_scale_test.dart +1 -1
- package/dart/test/co2_acceptance_test.dart +1 -1
- package/dart/test/dlq_session_surface_test.dart +1 -1
- package/dart/test/first_app_gauntlet_test.dart +1 -1
- package/dart/test/local_sync_inert_test.dart +1 -1
- package/dart/test/node_runner_transport_test.dart +3 -3
- package/dart/test/parent_signer_recovery_test.dart +1 -1
- package/dart/test/sealing_client_test.dart +1 -1
- package/dart/test/session_bound_test.dart +1 -1
- package/dart/test/test_harness_test.dart +1 -1
- package/dart/test/vo_read_typing_test.dart +1 -1
- package/dart/test/watch_after_offer_stall_test.dart +1 -1
- package/dart/test/watch_surfaces_test.dart +1 -1
- package/dart_types/CHANGELOG.md +4 -0
- package/dart_types/lib/src/errors.dart +27 -0
- package/package.json +1 -1
- package/src/build_package.ts +13 -1
- package/src/codegen_dart_client.ts +10 -0
- package/src/codegen_ts.ts +23 -4
- package/src/compat_manifest.ts +7 -0
- package/src/compile_package_main.ts +40 -0
- package/src/framework/birth_meter.ts +1 -1
- package/src/framework/home.ts +1 -1
- package/src/framework/workspaces.ts +1 -1
- package/src/index.ts +14 -0
- package/src/kernel_capabilities.ts +245 -0
|
@@ -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.
|
|
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.
|
|
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.
|
|
66
|
-
/// `require.resolve('@githolon/client/runner/node-entry.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
250
|
-
/// `require.resolve('@githolon/client/runner/node-entry.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
@@ -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.
|
|
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.
|
|
29
|
-
String get _installer => '$_root/cloud/web-client/runner/offline-install.
|
|
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.
|
|
25
|
-
String get _devServer => '$_root/cloud/dev-server/server.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
16
|
+
final p = '$here/../../cloud/web-client/runner/node-entry.ts';
|
|
17
17
|
if (!File(p).existsSync()) {
|
|
18
|
-
throw StateError('node-entry.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 =>
|
package/dart_types/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## 0.5.0
|
|
2
|
+
|
|
3
|
+
- Add typed `SourceSessionNotMounted` error (code `nomos.attested_read.source_not_mounted`) for the colocated attested-read fix — a foreign `read(q, {from})` whose source workspace isn't mounted now fails fast with a typed error + `mountSourceSession` remedy.
|
|
4
|
+
|
|
1
5
|
## 0.4.1
|
|
2
6
|
|
|
3
7
|
- `canonicalPrincipal` / `signerObjectOf` — THE one principal normalizer (kernel truth:
|
|
@@ -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.
|
|
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",
|
package/src/build_package.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
@@ -1086,6 +1099,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1086
1099
|
` * and the holon computes the LIVE per-member verdict on \`holon.compat\`. A KNOWN-incompatible directive`,
|
|
1087
1100
|
` * then refuses CLIENT-SIDE at write time (LawMismatchError), never as a server 422. */`,
|
|
1088
1101
|
` checkCompat?(compat: { lawHash: string; interfaceHash: string; packageName: string }, requirement?: unknown): NomosCompatVerdict | null;`,
|
|
1102
|
+
` /** RELEASE COHERENCE: throws a typed KernelCoherenceError at bind if the cloud's kernel lacks a`,
|
|
1103
|
+
` * capability this law requires (e.g. \`compoundKeys\`) — instead of an opaque deep fold refusal. */`,
|
|
1104
|
+
` assertKernelCoherence?(requiresKernel: readonly string[], opts?: { lawHash?: string }): unknown;`,
|
|
1089
1105
|
` /** The live compat verdict (set by checkCompat). \`has(memberId)\` is the dark-launch feature flag;`,
|
|
1090
1106
|
` * \`refresh()\` re-derives against the current projection. Absent until the factory binds. */`,
|
|
1091
1107
|
` compat?: NomosCompatVerdict;`,
|
|
@@ -1483,7 +1499,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1483
1499
|
for (const [, c] of [...contractByIdTs].sort(([a], [b]) => a.localeCompare(b))) {
|
|
1484
1500
|
out.push(`export interface ${c.id}Contract {`);
|
|
1485
1501
|
for (const [name, field] of Object.entries(c.fields)) {
|
|
1486
|
-
out.push(` ${name}?: ${tsTypeOfField(field as Field)};`);
|
|
1502
|
+
out.push(` ${tsKey(name)}?: ${tsTypeOfField(field as Field)};`);
|
|
1487
1503
|
}
|
|
1488
1504
|
out.push(`}`, ``);
|
|
1489
1505
|
}
|
|
@@ -1522,14 +1538,14 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1522
1538
|
const required = (field as Field).isFromBirth === true;
|
|
1523
1539
|
const doc = nullDocLine(field);
|
|
1524
1540
|
if (doc !== undefined) out.push(` /** ${doc} */`);
|
|
1525
|
-
out.push(` ${name}${required ? "" : "?"}: ${tsTypeOfField(field as Field)};`);
|
|
1541
|
+
out.push(` ${tsKey(name)}${required ? "" : "?"}: ${tsTypeOfField(field as Field)};`);
|
|
1526
1542
|
const fr = (field as { foreignRef?: { workspaceField: string; targetAggregateId: string } }).foreignRef;
|
|
1527
1543
|
if (fr !== undefined) foreignRefFields.push({ agg: typeId, field: name, workspaceField: fr.workspaceField, target: fr.targetAggregateId });
|
|
1528
1544
|
}
|
|
1529
1545
|
if (attributed.length > 0) {
|
|
1530
1546
|
out.push(
|
|
1531
1547
|
` /** attributed-read provenance (the reserved per-row map; prefer the generated accessors) */`,
|
|
1532
|
-
` "__prov"?: { ${attributed.map((f) => `${f}?: FieldProvenance`).join("; ")} };`,
|
|
1548
|
+
` "__prov"?: { ${attributed.map((f) => `${tsKey(f)}?: FieldProvenance`).join("; ")} };`,
|
|
1533
1549
|
);
|
|
1534
1550
|
}
|
|
1535
1551
|
for (const extra of extrasByType.get(typeId) ?? []) out.push(extra);
|
|
@@ -1542,7 +1558,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1542
1558
|
` * unknown-author fields omitted). */`,
|
|
1543
1559
|
`export const ${ns}Provenance = {`,
|
|
1544
1560
|
...attributed.map(
|
|
1545
|
-
(f) => ` ${f}: (data: ${typeId}Data): FieldProvenance | undefined => data.__prov
|
|
1561
|
+
(f) => ` ${tsKey(f)}: (data: ${typeId}Data): FieldProvenance | undefined => data.__prov?${tsOptAccess(f)},`,
|
|
1546
1562
|
),
|
|
1547
1563
|
` lastAuthors: (data: ${typeId}Data): Record<string, string> => {`,
|
|
1548
1564
|
` const out: Record<string, string> = {};`,
|
|
@@ -2109,6 +2125,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
2109
2125
|
opts.interface !== undefined
|
|
2110
2126
|
? ` holon.checkCompat?.(${compatConstName(opts.packageName)}, ${interfaceConstName(opts.packageName)});`
|
|
2111
2127
|
: ` holon.checkCompat?.(${compatConstName(opts.packageName)});`,
|
|
2128
|
+
// RELEASE COHERENCE: throw a typed skew error AT BIND if the cloud's kernel lacks a capability
|
|
2129
|
+
// this law requires (e.g. compoundKeys) — instead of the opaque deep fold refusal later.
|
|
2130
|
+
` holon.assertKernelCoherence?.(${compatConstName(opts.packageName)}.requiresKernel ?? [], { lawHash: ${compatConstName(opts.packageName)}.lawHash });`,
|
|
2112
2131
|
]
|
|
2113
2132
|
: []),
|
|
2114
2133
|
` return {`,
|
package/src/compat_manifest.ts
CHANGED
|
@@ -17,4 +17,11 @@ export interface NomosCompat {
|
|
|
17
17
|
readonly domains: Record<string, { readonly identityHash?: string }>;
|
|
18
18
|
/** The generator provenance — the `@githolon/dsl` version + this manifest's schema. */
|
|
19
19
|
readonly generator: { readonly dsl: string; readonly schema: number };
|
|
20
|
+
/** RELEASE COHERENCE (kernel_capabilities.ts): the named kernel capabilities this law
|
|
21
|
+
* REQUIRES to fold (derived from its shape — e.g. `compoundKeys` for a `.key(a, b)` query).
|
|
22
|
+
* connect() / deploy compares this against the target kernel's advertised capabilities and
|
|
23
|
+
* fails with a TYPED message naming the mismatch, INSTEAD of the opaque deep-fold refusal.
|
|
24
|
+
* Empty ⇒ no special capability needed (byte-identical to before this field for such a law
|
|
25
|
+
* — but note: this manifest is frontend-only, never in the USD IR / domainHash). */
|
|
26
|
+
readonly requiresKernel: readonly string[];
|
|
20
27
|
}
|
|
@@ -132,6 +132,7 @@ import { domainInterface, type DomainInterface } from "./shape_compat.js";
|
|
|
132
132
|
import { generateTsProof, proofLegsIndex } from "./codegen_proof.js";
|
|
133
133
|
import { checkDomainModules, formatFindings, hasRefusal } from "./law_check.js";
|
|
134
134
|
import type { NomosCompat } from "./compat_manifest.js";
|
|
135
|
+
import { requiredKernelFeatures, type FeatureProbeModule } from "./kernel_capabilities.js";
|
|
135
136
|
|
|
136
137
|
const DSL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
137
138
|
|
|
@@ -2552,12 +2553,26 @@ async function main(): Promise<void> {
|
|
|
2552
2553
|
const identityHash = identity.hashes[key];
|
|
2553
2554
|
compatDomains[key] = identityHash === undefined ? {} : { identityHash };
|
|
2554
2555
|
}
|
|
2556
|
+
// RELEASE COHERENCE — the kernel capabilities this law REQUIRES (compound-key queries +
|
|
2557
|
+
// era-key features). Baked into the client so connect() can check it against the target
|
|
2558
|
+
// kernel's advertised capabilities BEFORE the law folds (kernel_capabilities.ts).
|
|
2559
|
+
const requiresKernel = ((): string[] => {
|
|
2560
|
+
const probes: FeatureProbeModule[] = domainModules.map((m) => (m.queries ? { queries: m.queries } : {}));
|
|
2561
|
+
try {
|
|
2562
|
+
const parsed = JSON.parse(usdJson) as { layers?: Record<string, unknown>[] };
|
|
2563
|
+
for (const layer of parsed.layers ?? []) probes.push({ layer });
|
|
2564
|
+
} catch {
|
|
2565
|
+
/* shape-derived features (compoundKeys) still apply from the queries above */
|
|
2566
|
+
}
|
|
2567
|
+
return requiredKernelFeatures(probes);
|
|
2568
|
+
})();
|
|
2555
2569
|
const nomosCompat: NomosCompat = {
|
|
2556
2570
|
packageName: cfg.name,
|
|
2557
2571
|
lawHash: domainHash,
|
|
2558
2572
|
interfaceHash: sha256HexUtf8(canonicalJson(packageInterface)),
|
|
2559
2573
|
domains: compatDomains,
|
|
2560
2574
|
generator: { dsl: dslVersion, schema: 1 },
|
|
2575
|
+
requiresKernel,
|
|
2561
2576
|
};
|
|
2562
2577
|
|
|
2563
2578
|
// ── 3b. the OpenAPI spec — THE PACKAGE'S SELF-DESCRIPTION (the host's runtime routing table;
|
|
@@ -2766,9 +2781,34 @@ async function main(): Promise<void> {
|
|
|
2766
2781
|
// files would false-fail every vendored fixture. Analyze the in-package set; note the loose remainder.
|
|
2767
2782
|
const inPackage = emittedDart.filter((f) => pkgDirOf(f) !== undefined);
|
|
2768
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
|
+
})();
|
|
2769
2802
|
let dartMissing = false;
|
|
2770
2803
|
let unresolved: string | undefined;
|
|
2771
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
|
+
}
|
|
2772
2812
|
try {
|
|
2773
2813
|
execFileSync("dart", ["pub", "get"], { cwd: pkg, stdio: "pipe", encoding: "utf8" });
|
|
2774
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
|
|
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
|
*
|
package/src/framework/home.ts
CHANGED
|
@@ -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 `
|
|
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
|
|
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
|
package/src/index.ts
CHANGED
|
@@ -148,6 +148,20 @@ export { repair, repairsOf, isRepairDecl, type RepairDecl, type RepairMatcher, t
|
|
|
148
148
|
// `nomos-compile` REFUSES law that references read() (a named compile error, never a runtime halt).
|
|
149
149
|
// The captured-reads wave (task #58) wires the capability in and lifts the refusal.
|
|
150
150
|
export { read } from "./read.js";
|
|
151
|
+
// RELEASE COHERENCE — the kernel capability stamp + law↔kernel fit check (kernel_capabilities.ts).
|
|
152
|
+
export {
|
|
153
|
+
requiredKernelFeatures,
|
|
154
|
+
capabilitiesForKernelHash,
|
|
155
|
+
assessKernelCoherence,
|
|
156
|
+
KernelCoherenceError,
|
|
157
|
+
KERNEL_CAPABILITY_REGISTRY,
|
|
158
|
+
ERA_KEY_TO_FEATURE,
|
|
159
|
+
type KernelFeature,
|
|
160
|
+
type KernelStamp,
|
|
161
|
+
type KernelCoherenceVerdict,
|
|
162
|
+
type KernelRegistryEntry,
|
|
163
|
+
type FeatureProbeModule,
|
|
164
|
+
} from "./kernel_capabilities.js";
|
|
151
165
|
// ATTESTED CROSS-WORKSPACE READS (slice B — attested_reads_design.md): the declared trust
|
|
152
166
|
// surface (`trustSource`), the foreign query reference, and the reader-side SourceFrontier.
|
|
153
167
|
export {
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// RELEASE COHERENCE — the kernel capability stamp + the law↔kernel fit check.
|
|
2
|
+
//
|
|
3
|
+
// THE PROBLEM THIS SOLVES (the co2 skew, 2026-07): a published release can be
|
|
4
|
+
// internally inconsistent. The production kernel accepts law that a compiler emits;
|
|
5
|
+
// the DEV/test-mint kernel a `githolon cloud dev` serves can be STALE and REFUSE the
|
|
6
|
+
// same law — but only at runtime, deep in a law fold ("query declares 2 index keys;
|
|
7
|
+
// the read router supports exactly one"). Four artifacts drift independently:
|
|
8
|
+
// · the compiler (@githolon/dsl) that EMITS the law,
|
|
9
|
+
// · the runner bundle (@githolon/client) that RUNS it,
|
|
10
|
+
// · the production kernel wasm the cloud serves,
|
|
11
|
+
// · the test-mint kernel wasm the dev cloud serves.
|
|
12
|
+
// There is NO version/hash check that catches the skew before the opaque refusal.
|
|
13
|
+
//
|
|
14
|
+
// THE FIX: a kernel advertises a VERSION + a set of named CAPABILITIES (feature flags
|
|
15
|
+
// keyed to the era keys / read-router lanes it implements). A package DECLARES the
|
|
16
|
+
// kernel capabilities its law REQUIRES (derived from its shape — e.g. a compound-key
|
|
17
|
+
// query needs `compoundKeys`). Before the law ever folds, we compare the two and fail
|
|
18
|
+
// with a TYPED, human-legible message NAMING the mismatch — not the deep refusal.
|
|
19
|
+
//
|
|
20
|
+
// This module is the SINGLE SOURCE OF TRUTH for: the capability vocabulary, deriving a
|
|
21
|
+
// package's requirements from its shape, the known-kernel registry (keyed by wasm
|
|
22
|
+
// sha256), and the coherence assessment. The CLI (`githolon doctor`, deploy), the
|
|
23
|
+
// generated client (baked `requiresKernel`), and the cloud worker's
|
|
24
|
+
// `/v1/runtime/capabilities` endpoint all draw on it.
|
|
25
|
+
|
|
26
|
+
import type { QueryDecl } from "./query.js";
|
|
27
|
+
|
|
28
|
+
/** A named kernel capability — a feature/lane the wasm kernel must implement for a law
|
|
29
|
+
* that uses it to fold. Each maps to a read-router lane or an era key. Additive: a new
|
|
30
|
+
* capability name is minted here the same day the kernel arm ships. */
|
|
31
|
+
export type KernelFeature =
|
|
32
|
+
/** The read router accepts a query with MORE THAN ONE index key (`.key(a, b, …)`),
|
|
33
|
+
* routing it as a compound (AND) intersection. Pre-this kernels refuse >1 index key
|
|
34
|
+
* ("the read router supports exactly one"). THE CONCRETE co2 SKEW. */
|
|
35
|
+
| "compoundKeys"
|
|
36
|
+
/** The #58 captured-read lane (`read()` in deployable law + `.reads(query)`). */
|
|
37
|
+
| "capturedReads"
|
|
38
|
+
/** The #41 declared-invariant gate (`invariant` / `workspaceInvariant`). */
|
|
39
|
+
| "invariantGate"
|
|
40
|
+
/** Surfaced(inner) — retained concurrent competing values as a pending set. */
|
|
41
|
+
| "surfacedMerge"
|
|
42
|
+
/** `.attributed()` / aggregate `{attributed:true}` per-field provenance. */
|
|
43
|
+
| "fieldProvenance"
|
|
44
|
+
/** Null as an opt-in named capability (`admitsNull()`). */
|
|
45
|
+
| "nullSemantics"
|
|
46
|
+
/** Dynamic attested reads under a trustPolicy allowList. */
|
|
47
|
+
| "attestedReads";
|
|
48
|
+
|
|
49
|
+
/** The USD-IR era key each derived-from-law capability rides on, for the requirement
|
|
50
|
+
* derivation below (a package that emits the era key needs the capability). */
|
|
51
|
+
export const ERA_KEY_TO_FEATURE: Readonly<Record<string, KernelFeature>> = {
|
|
52
|
+
nomosReadGate: "capturedReads",
|
|
53
|
+
nomosInvariantGate: "invariantGate",
|
|
54
|
+
nomosSurfaced: "surfacedMerge",
|
|
55
|
+
nomosAttributed: "fieldProvenance",
|
|
56
|
+
nomosNullable: "nullSemantics",
|
|
57
|
+
nomosAttestedReads: "attestedReads",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/** A minimal shape of a compiled domain module — enough to derive its kernel needs. */
|
|
61
|
+
export interface FeatureProbeModule {
|
|
62
|
+
readonly queries?: readonly QueryDecl[];
|
|
63
|
+
/** The domain's USD-IR layer (the era keys ride here). Optional — shape-derived
|
|
64
|
+
* features (compoundKeys) work without it. */
|
|
65
|
+
readonly layer?: Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** DERIVE the kernel capabilities a package's law REQUIRES, from its shape + era keys.
|
|
69
|
+
* The result is a sorted, de-duped list baked into the client compat manifest and
|
|
70
|
+
* checked against a target kernel's advertised capabilities before the law folds.
|
|
71
|
+
*
|
|
72
|
+
* The load-bearing case: ANY query with a compound (`.key(a, b)`) index needs
|
|
73
|
+
* `compoundKeys` — exactly the lane the stale test-mint kernel lacked. */
|
|
74
|
+
export function requiredKernelFeatures(modules: readonly FeatureProbeModule[]): KernelFeature[] {
|
|
75
|
+
const needs = new Set<KernelFeature>();
|
|
76
|
+
for (const m of modules) {
|
|
77
|
+
for (const q of m.queries ?? []) {
|
|
78
|
+
if (Array.isArray(q.key) && q.key.length > 1) needs.add("compoundKeys");
|
|
79
|
+
}
|
|
80
|
+
if (m.layer) {
|
|
81
|
+
for (const [eraKey, feature] of Object.entries(ERA_KEY_TO_FEATURE)) {
|
|
82
|
+
if (m.layer[eraKey] !== undefined) needs.add(feature);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return [...needs].sort();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A kernel's self-description: its version tag + the capabilities it implements. This is
|
|
90
|
+
* the shape the kernel stamps (see `kernelStamp` op in wasm_git_holon.rs) AND the shape
|
|
91
|
+
* the host `/v1/runtime/capabilities` endpoint synthesizes for the deployed wasm it can't
|
|
92
|
+
* yet ask (keyed by the wasm sha256 in the registry below). */
|
|
93
|
+
export interface KernelStamp {
|
|
94
|
+
/** The kernel semver-ish version tag baked into the wasm at build. */
|
|
95
|
+
readonly version: string;
|
|
96
|
+
/** The wasm artifact sha256 (64-hex), when known. */
|
|
97
|
+
readonly hash?: string;
|
|
98
|
+
/** The named capabilities this kernel implements. */
|
|
99
|
+
readonly features: readonly KernelFeature[];
|
|
100
|
+
/** Human note (e.g. "production", "test-mint (DEV ONLY)") — surfaced in the skew message. */
|
|
101
|
+
readonly note?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** THE KNOWN-KERNEL REGISTRY — maps a kernel wasm sha256 PREFIX to its capability stamp.
|
|
105
|
+
* Keyed by prefix so a short hash (the co2 report cites `846c6bf8` / `f81cb952`) resolves.
|
|
106
|
+
*
|
|
107
|
+
* WHY host-side and not only asked of the kernel: the CURRENTLY-DEPLOYED stale kernel
|
|
108
|
+
* predates the stamp op — it cannot answer "what are your capabilities?". Until every
|
|
109
|
+
* serving kernel carries the in-wasm stamp, this registry lets the host + client resolve
|
|
110
|
+
* a deployed wasm's capabilities from its BYTES, which is always available. A kernel that
|
|
111
|
+
* DOES carry the stamp op is preferred; this is the fallback that works today.
|
|
112
|
+
*
|
|
113
|
+
* Add an entry the moment a new kernel wasm is cut (its sha256 + the features it gained). */
|
|
114
|
+
export interface KernelRegistryEntry extends KernelStamp {
|
|
115
|
+
readonly prefix: string;
|
|
116
|
+
/** Human note (e.g. "production", "test-mint (DEV ONLY)"). */
|
|
117
|
+
readonly note?: string;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export const KERNEL_CAPABILITY_REGISTRY: readonly KernelRegistryEntry[] = [
|
|
121
|
+
{
|
|
122
|
+
prefix: "846c6bf8",
|
|
123
|
+
version: "kernel-846c6bf8",
|
|
124
|
+
note: "production (accepts compound-key law)",
|
|
125
|
+
features: [
|
|
126
|
+
"compoundKeys",
|
|
127
|
+
"capturedReads",
|
|
128
|
+
"invariantGate",
|
|
129
|
+
"surfacedMerge",
|
|
130
|
+
"fieldProvenance",
|
|
131
|
+
"nullSemantics",
|
|
132
|
+
"attestedReads",
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
// CURRENT test-mint kernel (rebuilt from the SAME source as prod 846c6bf8, 2026-07-14; staged to R2 +
|
|
137
|
+
// built/staged by deploy.sh every release). Feature-equivalent to production — the dev cloud is coherent.
|
|
138
|
+
prefix: "f6e44b3a",
|
|
139
|
+
version: "kernel-f6e44b3a",
|
|
140
|
+
note: "test-mint (DEV) — current, feature-equivalent to production",
|
|
141
|
+
features: [
|
|
142
|
+
"compoundKeys",
|
|
143
|
+
"capturedReads",
|
|
144
|
+
"invariantGate",
|
|
145
|
+
"surfacedMerge",
|
|
146
|
+
"fieldProvenance",
|
|
147
|
+
"nullSemantics",
|
|
148
|
+
"attestedReads",
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
prefix: "f81cb952",
|
|
153
|
+
version: "kernel-f81cb952",
|
|
154
|
+
note: "test-mint (DEV ONLY — STALE: predates compound-key read router; superseded by f6e44b3a)",
|
|
155
|
+
features: [
|
|
156
|
+
"capturedReads",
|
|
157
|
+
"invariantGate",
|
|
158
|
+
"surfacedMerge",
|
|
159
|
+
"fieldProvenance",
|
|
160
|
+
"nullSemantics",
|
|
161
|
+
"attestedReads",
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
/** Resolve a kernel wasm hash (full or short) to its known capability stamp, or `null`
|
|
167
|
+
* when the hash is unknown to the registry (a kernel we've never catalogued). */
|
|
168
|
+
export function capabilitiesForKernelHash(hash: string | undefined | null): KernelStamp | null {
|
|
169
|
+
if (typeof hash !== "string" || hash.length < 6) return null;
|
|
170
|
+
const h = hash.toLowerCase().replace(/^sha256:|^policy:/, "");
|
|
171
|
+
const hit = KERNEL_CAPABILITY_REGISTRY.find((e) => h.startsWith(e.prefix) || e.prefix.startsWith(h));
|
|
172
|
+
return hit
|
|
173
|
+
? { version: hit.version, hash: h, features: hit.features, ...(hit.note ? { note: hit.note } : {}) }
|
|
174
|
+
: null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** The verdict of a law↔kernel coherence assessment. */
|
|
178
|
+
export interface KernelCoherenceVerdict {
|
|
179
|
+
readonly ok: boolean;
|
|
180
|
+
/** The required features the target kernel does NOT advertise. Empty ⇒ coherent. */
|
|
181
|
+
readonly missing: KernelFeature[];
|
|
182
|
+
/** A human-legible one-line message — the thing surfaced INSTEAD of the deep fold refusal. */
|
|
183
|
+
readonly message: string;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** ASSESS whether a target kernel can fold a package's law. `requires` is the package's
|
|
187
|
+
* derived requirement ({@link requiredKernelFeatures}); `kernel` is the target's stamp
|
|
188
|
+
* (from its stamp op, or {@link capabilitiesForKernelHash}). A `null` kernel stamp means
|
|
189
|
+
* the target's capabilities are UNKNOWN — reported distinctly (can't prove coherence).
|
|
190
|
+
*
|
|
191
|
+
* The message names BOTH hashes and the offending capability — the co2 skew, legibly. */
|
|
192
|
+
export function assessKernelCoherence(params: {
|
|
193
|
+
requires: readonly KernelFeature[];
|
|
194
|
+
kernel: KernelStamp | null;
|
|
195
|
+
/** The target kernel's hash, for the message even when the stamp is unknown. */
|
|
196
|
+
kernelHash?: string;
|
|
197
|
+
/** The law's hash, for the message. */
|
|
198
|
+
lawHash?: string;
|
|
199
|
+
}): KernelCoherenceVerdict {
|
|
200
|
+
const { requires, kernel } = params;
|
|
201
|
+
const short = (h?: string) => (h ? `${String(h).replace(/^sha256:|^policy:/, "").slice(0, 8)}` : "<unknown>");
|
|
202
|
+
const lawH = short(params.lawHash);
|
|
203
|
+
const kernH = short(params.kernelHash ?? kernel?.hash);
|
|
204
|
+
if (requires.length === 0) {
|
|
205
|
+
return { ok: true, missing: [], message: `law ${lawH} requires no special kernel capabilities` };
|
|
206
|
+
}
|
|
207
|
+
if (kernel === null) {
|
|
208
|
+
return {
|
|
209
|
+
ok: false,
|
|
210
|
+
missing: [...requires],
|
|
211
|
+
message:
|
|
212
|
+
`cannot verify release coherence — this cloud's kernel ${kernH} is UNKNOWN to the ` +
|
|
213
|
+
`capability registry, so it cannot be confirmed to support [${requires.join(", ")}] that ` +
|
|
214
|
+
`law ${lawH} requires. Update the kernel registry or upgrade the cloud's kernel.`,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const missing = requires.filter((f) => !kernel.features.includes(f)) as KernelFeature[];
|
|
218
|
+
if (missing.length === 0) {
|
|
219
|
+
return { ok: true, missing: [], message: `kernel ${kernH} supports all capabilities law ${lawH} requires` };
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
ok: false,
|
|
223
|
+
missing,
|
|
224
|
+
message:
|
|
225
|
+
`this cloud's kernel ${kernH} lacks capability '${missing.join("', '")}' that law ${lawH} requires — ` +
|
|
226
|
+
`the cloud is running a STALE kernel (${kernel.note ?? kernel.version}). ` +
|
|
227
|
+
(missing.includes("compoundKeys")
|
|
228
|
+
? `A compound-key query (\`.key(a, b, …)\`) will be refused deep in a law fold with ` +
|
|
229
|
+
`"query declares N index keys; the read router supports exactly one". `
|
|
230
|
+
: ``) +
|
|
231
|
+
`Upgrade the cloud's kernel (or point at a cloud whose kernel is coherent with this release).`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A typed error thrown when a coherence assertion fails — carries the structured verdict so
|
|
236
|
+
* callers can render it or branch, instead of parsing a message. */
|
|
237
|
+
export class KernelCoherenceError extends Error {
|
|
238
|
+
readonly verdict: KernelCoherenceVerdict;
|
|
239
|
+
readonly code = "kernel-capability-skew";
|
|
240
|
+
constructor(verdict: KernelCoherenceVerdict) {
|
|
241
|
+
super(verdict.message);
|
|
242
|
+
this.name = "KernelCoherenceError";
|
|
243
|
+
this.verdict = verdict;
|
|
244
|
+
}
|
|
245
|
+
}
|