@githolon/dsl 0.101.5 → 0.102.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 (120) hide show
  1. package/dart/CHANGELOG.md +13 -0
  2. package/dart/lib/nomos_client.dart +3 -52
  3. package/dart/lib/src/dev_cloud.dart +5 -0
  4. package/dart/lib/src/home.dart +10 -15
  5. package/dart/lib/src/nms_conformance_runtime.dart +12 -0
  6. package/dart/lib/src/nms_generated_runtime.dart +29 -0
  7. package/dart/lib/src/nms_test_environment.dart +8 -0
  8. package/dart/lib/src/node_runner_transport.dart +56 -18
  9. package/dart/lib/src/provider.dart +5 -1
  10. package/dart/lib/src/record_origin.dart +55 -0
  11. package/dart/lib/src/runner/node-entry.mjs +9105 -0
  12. package/dart/lib/src/runner/offline-install.mjs +4729 -0
  13. package/dart/lib/src/session_bound.dart +23 -10
  14. package/dart/lib/src/source_trust.dart +51 -0
  15. package/dart/lib/src/test_harness.dart +490 -58
  16. package/dart/lib/src/webview_bridge.dart +167 -58
  17. package/dart/lib/src/workspace_name.dart +7 -1
  18. package/dart/pubspec.lock +3 -3
  19. package/dart/pubspec.yaml +4 -2
  20. package/dart/test/acquire_session_credentials_test.dart +34 -17
  21. package/dart/test/acquire_session_test.dart +109 -24
  22. package/dart/test/adv_realm_interference_test.dart +227 -99
  23. package/dart/test/bundle_version_pairing_test.dart +1 -1
  24. package/dart/test/cloud_bound_topology_test.dart +1 -1
  25. package/dart/test/cluster_attested_test.dart +52 -35
  26. package/dart/test/cluster_births_test.dart +5 -2
  27. package/dart/test/cluster_device_keyed_acceptance_test.dart +5 -2
  28. package/dart/test/cluster_local_autosync_test.dart +5 -2
  29. package/dart/test/cluster_scale_test.dart +5 -2
  30. package/dart/test/compat_test.dart +1 -1
  31. package/dart/test/dlq_session_surface_test.dart +2 -1
  32. package/dart/test/errors_test.dart +1 -1
  33. package/dart/test/first_app_gauntlet_test.dart +78 -51
  34. package/dart/test/four_layer_nested_topology_test.dart +1 -1
  35. package/dart/test/harness_phase_timeout_test.dart +1 -1
  36. package/dart/test/home_parity_test.dart +9 -5
  37. package/dart/test/local_sync_inert_test.dart +22 -14
  38. package/dart/test/node_runner_transport_test.dart +9 -5
  39. package/dart/test/null_slot_test.dart +1 -1
  40. package/dart/test/parent_signer_recovery_test.dart +5 -2
  41. package/dart/test/schema_validation_test.dart +1 -1
  42. package/dart/test/sealing_client_test.dart +2 -1
  43. package/dart/test/session_bound_test.dart +92 -119
  44. package/dart/test/stale_bundle_test.dart +1 -1
  45. package/dart/test/test_harness_test.dart +77 -22
  46. package/dart/test/vo_read_typing_test.dart +32 -14
  47. package/dart/test/watch_after_offer_stall_test.dart +2 -1
  48. package/dart/test/webview_bridge_test.dart +17 -4
  49. package/dart/test/wire_test.dart +1 -1
  50. package/dart_types/CHANGELOG.md +8 -0
  51. package/dart_types/lib/nomos_types.dart +7 -6
  52. package/dart_types/lib/src/activity.dart +23 -13
  53. package/dart_types/lib/src/foreign_ref.dart +61 -112
  54. package/dart_types/lib/src/invitation.dart +96 -0
  55. package/dart_types/lib/src/nomos_ref.dart +97 -59
  56. package/dart_types/pubspec.yaml +1 -1
  57. package/dart_types/test/surfaced_test.dart +125 -19
  58. package/package.json +7 -3
  59. package/runtime/generated-application-host.js +8851 -0
  60. package/src/activity.ts +2 -2
  61. package/src/aggregate.ts +24 -1
  62. package/src/application_composition.ts +282 -0
  63. package/src/application_topology.ts +151 -0
  64. package/src/attested_core.ts +2 -2
  65. package/src/authoring_context.ts +9 -0
  66. package/src/birth_meter_ensure.ts +1 -1
  67. package/src/births.ts +53 -5
  68. package/src/build_package.ts +79 -71
  69. package/src/business_birth.ts +93 -10
  70. package/src/clear_directive.ts +3 -1
  71. package/src/codegen_dart.ts +369 -420
  72. package/src/codegen_dart_application.ts +1508 -0
  73. package/src/codegen_dart_client.ts +156 -95
  74. package/src/codegen_dart_scenario.ts +299 -0
  75. package/src/codegen_openapi.ts +8 -3
  76. package/src/codegen_proof.ts +1113 -1137
  77. package/src/codegen_provider_dart.ts +12 -1
  78. package/src/codegen_ts.ts +1176 -496
  79. package/src/compat_package_main.ts +22 -10
  80. package/src/compile_package_main.ts +1536 -337
  81. package/src/contract.ts +10 -4
  82. package/src/dart_barrel_guard.ts +5 -1
  83. package/src/describe_model.ts +13 -5
  84. package/src/directive.ts +244 -5
  85. package/src/engine_entry.ts +56 -13
  86. package/src/engine_javascript.ts +31 -0
  87. package/src/evolve_preview.ts +3 -12
  88. package/src/framework/application_home.ts +24 -10
  89. package/src/framework/capability.ts +15 -1
  90. package/src/framework/domain_lifecycle.ts +8 -0
  91. package/src/framework/framework_aggregates.ts +2 -2
  92. package/src/framework/genesis.ts +11 -1
  93. package/src/framework/home.ts +178 -75
  94. package/src/framework/impure_capability.ts +24 -7
  95. package/src/framework/relations.ts +13 -5
  96. package/src/framework/workspaces.ts +13 -12
  97. package/src/generated_client_names.ts +42 -3
  98. package/src/idof_lint.ts +65 -13
  99. package/src/index.ts +46 -12
  100. package/src/intent.ts +587 -0
  101. package/src/law_check.ts +64 -16
  102. package/src/precondition.ts +16 -4
  103. package/src/read_projection_namespace.ts +2 -7
  104. package/src/recipient.ts +33 -0
  105. package/src/relation_schema.ts +93 -0
  106. package/src/secondary_ensure.ts +11 -2
  107. package/src/shape_compat.ts +11 -1
  108. package/src/usd.ts +30 -8
  109. package/src/usd_layers.ts +447 -246
  110. package/src/usd_state.ts +1 -1
  111. package/src/value_object.ts +49 -13
  112. package/src/wire_encode.ts +124 -26
  113. package/src/workspace_sharing.ts +269 -0
  114. package/src/zod_introspection.ts +84 -0
  115. package/dart/example/nomos_secure_key_store.dart +0 -82
  116. package/dart/example/nomos_webview_transport.dart +0 -63
  117. package/dart/lib/nomos_headless.dart +0 -50
  118. package/dart/lib/testing.dart +0 -14
  119. package/dart/test/co2_product_gauntlet_test.dart +0 -244
  120. package/src/framework/identity.ts +0 -537
@@ -0,0 +1,84 @@
1
+ import type { z } from "zod";
2
+
3
+ /**
4
+ * The small Zod reflection boundary shared by code generators and compile-time
5
+ * law probes. Keep this module dependency-free: wire encoding sits below every
6
+ * generator and must not import a generator merely to inspect a payload schema.
7
+ */
8
+ export type ZodInternalDef = {
9
+ type?: string | z.ZodTypeAny;
10
+ innerType?: z.ZodTypeAny;
11
+ element?: z.ZodTypeAny;
12
+ shape?: Record<string, z.ZodTypeAny> | (() => Record<string, z.ZodTypeAny>);
13
+ entries?: Record<string, string | number>;
14
+ options?: z.ZodTypeAny[];
15
+ value?: unknown;
16
+ values?: readonly unknown[];
17
+ valueType?: z.ZodTypeAny;
18
+ getter?: () => z.ZodTypeAny;
19
+ in?: z.ZodTypeAny;
20
+ out?: z.ZodTypeAny;
21
+ };
22
+
23
+ export function zodDef(zt: z.ZodTypeAny): ZodInternalDef {
24
+ const raw = zt as unknown as { _def?: ZodInternalDef; def?: ZodInternalDef };
25
+ return raw._def ?? raw.def ?? {};
26
+ }
27
+
28
+ export function zodKind(zt: z.ZodTypeAny): string {
29
+ const type = zodDef(zt).type;
30
+ return typeof type === "string" ? type : "unknown";
31
+ }
32
+
33
+ export function zodEnumValues(zt: z.ZodTypeAny): string[] {
34
+ const raw = zt as unknown as { options?: readonly unknown[] };
35
+ if (raw.options !== undefined) return raw.options.map(String);
36
+ const entries = zodDef(zt).entries;
37
+ if (entries !== undefined) return Object.values(entries).map(String);
38
+ throw new Error("zod introspection: enum has no values");
39
+ }
40
+
41
+ export function zodObjectShape(zt: z.ZodTypeAny): Record<string, z.ZodTypeAny> {
42
+ const shape = zodDef(zt).shape;
43
+ if (typeof shape === "function") return shape();
44
+ if (shape !== undefined) return shape;
45
+ const raw = zt as unknown as { shape?: Record<string, z.ZodTypeAny> };
46
+ if (raw.shape !== undefined) return raw.shape;
47
+ throw new Error("zod introspection: object has no shape");
48
+ }
49
+
50
+ export function zodArrayElement(zt: z.ZodTypeAny): z.ZodTypeAny {
51
+ const def = zodDef(zt);
52
+ if (def.element !== undefined) return def.element;
53
+ if (typeof def.type !== "string" && def.type !== undefined) return def.type;
54
+ throw new Error("zod introspection: array has no element type");
55
+ }
56
+
57
+ /** Shared lawful primitive samples for compile-time probes and generated examples. Keeping this below the
58
+ * generators prevents security discovery and proof generation from disagreeing on constrained payloads. */
59
+ const SUBJECT_SAMPLES: readonly string[] = [
60
+ "user:00000000-0000-0000-0000-000000000000",
61
+ `key:${"0".repeat(64)}`,
62
+ "Type:00000000-0000-0000-0000-000000000000",
63
+ "0".repeat(64),
64
+ ];
65
+ const STRING_SAMPLES: readonly string[] = [
66
+ "sample", "{}", "en", "en-GB", "AA", "A1", "a", "1", "2026-01-01",
67
+ "000001", "2026-01-01T00:00:00Z", "a@example.com", "https://example.com", "#000000",
68
+ "00000000-0000-0000-0000-000000000000", "example.com", "0".repeat(40), "0".repeat(64),
69
+ `nomos-invitation-${"0".repeat(64)}`,
70
+ ];
71
+ const NUMBER_SAMPLES: readonly number[] = [1, 0, -1, 2, 10, 100, 0.5, -90, 90, 180, -180, 1000000];
72
+
73
+ export function zodSampleString(zt: z.ZodTypeAny, hint: string): string {
74
+ const readable = `sample-${hint}`;
75
+ if (zt.safeParse(readable).success) return readable;
76
+ for (const candidate of SUBJECT_SAMPLES) if (zt.safeParse(candidate).success) return candidate;
77
+ for (const candidate of STRING_SAMPLES) if (zt.safeParse(candidate).success) return candidate;
78
+ throw new Error(`zod introspection: no lawful string sample for '${hint}'`);
79
+ }
80
+
81
+ export function zodSampleNumber(zt: z.ZodTypeAny): number {
82
+ for (const candidate of NUMBER_SAMPLES) if (zt.safeParse(candidate).success) return candidate;
83
+ throw new Error("zod introspection: no lawful number sample");
84
+ }
@@ -1,82 +0,0 @@
1
- // COPY THIS INTO YOUR FLUTTER APP. It is an EXAMPLE, not part of the `nomos_client` library: it depends on
2
- // `flutter_secure_storage`, which the pure-Dart `nomos_client` package deliberately does NOT depend on (so
3
- // the protocol logic stays Flutter-free and unit-testable). This is the ~40 lines of app-specific glue for
4
- // where your device secret lives — the E2E key CUSTODY policy is yours; the protocol plumbing is not.
5
- //
6
- // THE SEALING CLIENT (e2e_sealing_client.md): a generated `<Domain>Client(bridge, keyStore: ...)` seals
7
- // encrypted directive fields before dispatch + decrypts encrypted read fields automatically. The `keyStore`
8
- // is the ONE thing you supply. `WrappedKeyStore` (in `nomos_client`) is the default: it unwraps this
9
- // principal's on-chain WrappedKeys with a DEVICE SECRET. This example shows where that device secret lives —
10
- // the platform keychain / Keystore via `flutter_secure_storage` — so it survives app restarts, stays off the
11
- // chain, and never leaves the device.
12
- //
13
- // WIRING (once per signed-in principal):
14
- // final store = await SecureDeviceKeyStore.open(bridge: bridge, principal: myUid);
15
- // final app = NotesClient(bridge, actor: myUid, keyStore: store); // reads decrypt, writes seal
16
- // final rows = await app.notesByOwner(owner: myUid); // `body` comes back plaintext
17
- //
18
- // The OWNER ceremony (grantAndWrap / revokeAndRotate / rotate) uses the SAME device pubkey this store
19
- // publishes — see `<Domain>Client(bridge, actor: owner).encryption`.
20
-
21
- import 'dart:convert';
22
-
23
- import 'package:flutter_secure_storage/flutter_secure_storage.dart';
24
- import 'package:nomos_client/nomos_client.dart';
25
-
26
- /// A [NomosKeyStore] whose device secret is held in the platform keychain (iOS Keychain / Android Keystore)
27
- /// via `flutter_secure_storage`. It delegates the scope-key unwrap to the framework's [WrappedKeyStore]; the
28
- /// only thing it adds is DURABLE, PLATFORM-PROTECTED custody of the X25519 device secret.
29
- class SecureDeviceKeyStore implements NomosKeyStore {
30
- final WrappedKeyStore _inner;
31
-
32
- /// This device's X25519 PUBLIC key — publish it (publishEncKey) / hand it to the owner as `publicKey` on
33
- /// `grantAndWrap` so the scope key can be wrapped TO this device.
34
- final String devicePublicKey;
35
-
36
- SecureDeviceKeyStore._(this._inner, this.devicePublicKey);
37
-
38
- /// Open (or first-time provision) the store for `principal`. On first run it MINTS an X25519 device keypair
39
- /// in the kernel, persists the SECRET in secure storage, and returns the store; on later runs it reuses it.
40
- static Future<SecureDeviceKeyStore> open({
41
- required NomosBridge bridge,
42
- required String principal,
43
- FlutterSecureStorage storage = const FlutterSecureStorage(),
44
- }) async {
45
- final secretKey = 'nomos.enc.deviceSecret.$principal';
46
- final publicKey = 'nomos.enc.devicePublic.$principal';
47
- var secret = await storage.read(key: secretKey);
48
- var public = await storage.read(key: publicKey);
49
- if (secret == null || public == null) {
50
- // FIRST RUN: mint the device keypair (secret stays client-side, never on chain) and persist it.
51
- final kp = await bridge.cryptoKeygen();
52
- secret = (kp['secret'] ?? '').toString();
53
- public = (kp['public'] ?? '').toString();
54
- await storage.write(key: secretKey, value: secret);
55
- await storage.write(key: publicKey, value: public);
56
- }
57
- final inner = WrappedKeyStore(
58
- bridge: bridge, principal: principal, deviceSecret: secret);
59
- return SecureDeviceKeyStore._(inner, public);
60
- }
61
-
62
- @override
63
- Future<NomosScopeKey?> scopeKey(String scope) => _inner.scopeKey(scope);
64
-
65
- @override
66
- Future<List<NomosScopeKey>> readKeys(List<String> scopes) =>
67
- _inner.readKeys(scopes);
68
-
69
- /// Convenience: the JSON a `publishEncKey` payload carries (bind the device pubkey to this principal on
70
- /// chain so an owner's `grantAndWrap` can resolve it without you passing `publicKey` each time).
71
- Map<String, Object?> publishPayload({required String publishedAt}) => {
72
- 'principal': _inner.principal,
73
- 'encPublicKey': devicePublicKey,
74
- 'publishedAt': publishedAt,
75
- };
76
-
77
- @override
78
- String toString() => 'SecureDeviceKeyStore(${jsonEncode({
79
- 'principal': _inner.principal,
80
- 'devicePublicKey': devicePublicKey,
81
- })})';
82
- }
@@ -1,63 +0,0 @@
1
- // COPY THIS INTO YOUR FLUTTER APP. It is an EXAMPLE, not part of the `nomos_client` library: it depends on
2
- // `webview_flutter`, which the pure-Dart `nomos_client` package deliberately does NOT depend on (so the bridge
3
- // protocol logic stays Flutter-free and unit-testable). This ~50-line transport is the only app-specific glue.
4
- //
5
- // It implements `BridgeTransport` (from `package:nomos_client/nomos_client.dart`) over a `webview_flutter`
6
- // WebViewController loading the published `nomos-runner` bundle:
7
- // • host → runner: `controller.runJavaScript("window.__nomosSend(<json>)")`
8
- // • runner → host: a `JavaScriptChannel("NomosHost")` whose onMessage feeds the bridge's message stream.
9
- //
10
- // WIRING (in your widget's initState):
11
- // final controller = WebViewController()
12
- // ..setJavaScriptMode(JavaScriptMode.unrestricted);
13
- // final transport = WebViewTransport(controller); // registers the NomosHost channel
14
- // await controller.loadFlutterAsset('assets/nomos/nomos-runner.html');
15
- // final bridge = NomosBridge(transport);
16
- // await bridge.ready; // resolves on the runner's {event:ready}
17
- // await bridge.connect(cloud: 'https://nomos.captainapp.co.uk', workspace: myWs, clientId: deviceId);
18
- // final app = GuestbookClient(bridge);
19
- // final head = await app.signGuestbook(SignGuestbookPayload(author: 'ada', message: 'hi', mood: 'delighted'));
20
- // final rows = await bridge.query('entriesByAuthor', {'author': 'ada'});
21
- //
22
- // (Put the runner bundle at `assets/nomos/nomos-runner.{html,js}` and declare it under `flutter: assets:`.)
23
-
24
- import 'dart:async';
25
- import 'dart:convert';
26
-
27
- import 'package:nomos_client/nomos_client.dart';
28
- import 'package:webview_flutter/webview_flutter.dart';
29
-
30
- /// A [BridgeTransport] over a `webview_flutter` controller hosting the `nomos-runner` bundle.
31
- class WebViewTransport implements BridgeTransport {
32
- final WebViewController controller;
33
- final _incoming = StreamController<Map<String, Object?>>.broadcast();
34
-
35
- WebViewTransport(this.controller) {
36
- // runner → host: the bundle posts JSON strings on the "NomosHost" channel.
37
- controller.addJavaScriptChannel(
38
- 'NomosHost',
39
- onMessageReceived: (JavaScriptMessage m) {
40
- try {
41
- final decoded = jsonDecode(m.message);
42
- if (decoded is Map) _incoming.add(decoded.cast<String, Object?>());
43
- } catch (_) {/* a non-JSON console line — ignore */}
44
- },
45
- );
46
- }
47
-
48
- @override
49
- Stream<Map<String, Object?>> get messages => _incoming.stream;
50
-
51
- @override
52
- void send(Map<String, Object?> message) {
53
- // host → runner: hand the JSON to the bundle. Double-encode so it survives as a JS string literal.
54
- final json = jsonEncode(message);
55
- controller.runJavaScript('window.__nomosSend(${jsonEncode(json)})');
56
- }
57
-
58
- /// Call once, right after the page finishes loading (e.g. from `NavigationDelegate.onPageFinished`), so the
59
- /// runner flushes any message (the initial `{event:"ready"}`) it emitted before this channel was wired.
60
- void flushReady() => controller.runJavaScript('window.__nomosReady && window.__nomosReady()');
61
-
62
- void dispose() => _incoming.close();
63
- }
@@ -1,50 +0,0 @@
1
- /// The HEADLESS transport library — `dart:io` only, so it lives outside the main
2
- /// `package:nomos_client/nomos_client.dart` barrel (which stays web-safe).
3
- ///
4
- /// MULTI-WORKSPACE BY DESIGN. A [NomosBridge] is a device's LOCAL HOLARCHY PLANE: one bridge / one
5
- /// transport / ONE shared engine plane, and MANY live workspaces mounted on it (the home, each estate, each
6
- /// catalogue, locally-born children) — N workspaces on one wasm engine, not N engines. Each workspace is a
7
- /// LOCAL githolon — cloud sync is an optional per-workspace posture, not a prerequisite.
8
- ///
9
- /// LOCAL BIRTHS OPEN IN-PLACE. A `.births()` directive offered into one workspace writes the child's custody
10
- /// straight into this same plane; the child is resident the moment it is born, so `bridge.connect(session:
11
- /// child)` (no `restoreFrom`) mounts it IN-PLACE — no re-clone — and reads/writes flow into the born child at
12
- /// once. (Proof: dsl/dart/test/cluster_births_test.dart; JS twin: cloud/web-client/test/realm.e2e.mjs.)
13
- ///
14
- /// Open workspaces and hold their [NomosSession] handles:
15
- ///
16
- /// ```dart
17
- /// import 'package:nomos_client/nomos_client.dart';
18
- /// import 'package:nomos_client/nomos_headless.dart';
19
- ///
20
- /// final transport = await NodeRunnerTransport.start(resolveFrom: 'path/to/app');
21
- /// final bridge = NomosBridge(transport);
22
- /// await bridge.ready;
23
- /// // open each workspace on the plane (local-only here — no `cloud` needed):
24
- /// await bridge.connect(workspace: 'home', session: 'home', restoreFrom: homeBytes);
25
- /// await bridge.connect(workspace: 'estate-42', session: 'estate-42', restoreFrom: estateBytes);
26
- /// // bind a generated client to a workspace via its session handle:
27
- /// final estate = EstateClient(bridge.session('estate-42'));
28
- /// final plane = await bridge.listWorkspaces(); // enumerate + sync posture
29
- /// ```
30
- ///
31
- /// The single-workspace `bridge.connect(...)` / `bridge.query(...)` methods are sugar over an inferred
32
- /// default session (unchanged). For TESTS, [NomosTestHarness] is a thin wrapper: `.ephemeral(...)` installs
33
- /// one law + opens one session; `.cluster(...)` installs N laws + opens N sessions on ONE plane — it adds
34
- /// no session logic of its own, it just drives the same production [NomosBridge.session] surface.
35
- ///
36
- /// The runner child process is the SAME `@githolon/client` every peer runs; the
37
- /// generated Dart clients drive it through [NomosBridge] exactly as in the app.
38
- library;
39
-
40
- export 'src/node_runner_transport.dart';
41
- export 'src/test_harness.dart';
42
- export 'src/dev_cloud.dart'; // NomosTestCloud (co2 #4 real-sync harness)
43
- // THE FULL SESSION SURFACE (task #86 gap 1): a headless test types everything the harness hands out —
44
- // [NomosSessionCredentials] (the `acquireSession` ifAbsentCredentials callback's return type),
45
- // [NomosBridge]/[NomosSession]/[NomosBbox]/[NomosCompat], the E2E sealing client
46
- // ([NomosKeyStore]/[WrappedKeyStore]/[NomosEncryption]), and the typed session-bound errors —
47
- // WITHOUT also importing the web barrel. Both files are web-safe (they are exported from
48
- // `nomos_client.dart` too); re-exporting them here just makes the headless import self-sufficient.
49
- export 'src/webview_bridge.dart';
50
- export 'src/session_bound.dart';
@@ -1,14 +0,0 @@
1
- // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
- // remotes are replaceable custody/transport, not truth.
3
- //
4
- /// The TEST-ONLY entrypoint (`package:nomos_client/testing.dart`) — the headless harness
5
- /// (NomosTestHarness drives the REAL githolon under `dart test`, no WebView) and the
6
- /// node-runner transport it rides. Depend on this ONLY from `dev_dependencies`-scoped
7
- /// packages: it pulls `dart:io` and process management, which never belong in app code.
8
- /// The layered-packages emit (`dart.packages.test`) re-exports this as the generated
9
- /// test package's harness surface.
10
- library;
11
-
12
- export 'src/node_runner_transport.dart';
13
- export 'src/test_harness.dart';
14
- export 'src/dev_cloud.dart'; // NomosTestCloud (co2 #4 real-sync harness)
@@ -1,244 +0,0 @@
1
- // Product acceptance: application → home → Estate, then Site → Asset, through the generated Dart client.
2
- // The harness owns the test cloud and framework ceremony; the application section supplies business facts.
3
- import 'dart:convert';
4
- import 'dart:io';
5
-
6
- import 'package:nomos_client/nomos_headless.dart';
7
- import 'package:test/test.dart';
8
-
9
- import '../../../examples/co2-birth/build/dart/src/generated/co2.client.dart';
10
-
11
- String get _root => '${Directory.current.path}/../..';
12
- String get _runner =>
13
- Platform.environment['NOMOS_NODE_ENTRY'] ??
14
- '$_root/cloud/web-client/runner/node-entry.ts';
15
- String get _server => '$_root/cloud/dev-server/server.ts';
16
- String get _deploy =>
17
- Platform.environment['NOMOS_CO2_DEPLOY'] ??
18
- '$_root/examples/co2-birth/build/co2.deploy.json';
19
- const _subject = 'user:co2-product-owner';
20
-
21
- Future<T> _eventually<T>(
22
- String description,
23
- Future<T?> Function() probe, {
24
- Duration timeout = const Duration(seconds: 90),
25
- }) async {
26
- final deadline = DateTime.now().add(timeout);
27
- Object? lastError;
28
- while (DateTime.now().isBefore(deadline)) {
29
- try {
30
- final value = await probe();
31
- if (value != null) return value;
32
- } catch (error) {
33
- lastError = error;
34
- }
35
- await Future<void>.delayed(const Duration(milliseconds: 250));
36
- }
37
- throw TestFailure(
38
- '$description did not become true within ${timeout.inSeconds}s${lastError == null ? '' : ': $lastError'}');
39
- }
40
-
41
- void main() {
42
- test(
43
- 'CO2 product journey: application → home → Estate → Site → Asset, offline then remotely durable',
44
- () async {
45
- final required = Platform.environment['NOMOS_GAUNTLET_REQUIRED'] == '1';
46
- final missing = [_runner, _server, _deploy]
47
- .where((path) => !File(path).existsSync())
48
- .toList();
49
- if (missing.isNotEmpty) {
50
- if (required)
51
- fail(
52
- 'CO2 product gauntlet prerequisites are missing: ${missing.join(', ')}');
53
- markTestSkipped(
54
- 'CO2 product gauntlet prerequisites are missing: ${missing.join(', ')}');
55
- return;
56
- }
57
-
58
- final diagnostic = Platform.environment['NOMOS_HARNESS_DIAG'] == '1'
59
- ? (String line) => stderr.writeln(line)
60
- : null;
61
- final cloud = await NomosTestCloud.start(
62
- serverEntry: _server,
63
- platform: 'co2-product-platform',
64
- deployJsonPath: _deploy,
65
- onDiagnostic: diagnostic,
66
- );
67
- NomosTestHarness? harness;
68
- try {
69
- // Release coherence is part of sync correctness, not an optional diagnostic. If the cloud cannot
70
- // identify the kernel that will receive these ledgers, a successful local write is not an adequate
71
- // product proof: the client would be synchronising into an unknown fold implementation.
72
- final capabilitiesHttp = HttpClient();
73
- try {
74
- final request = await capabilitiesHttp
75
- .getUrl(Uri.parse('${cloud.origin}/v1/runtime/capabilities'));
76
- final response = await request.close();
77
- final text = await response.transform(utf8.decoder).join();
78
- expect(response.statusCode, HttpStatus.ok,
79
- reason:
80
- 'the cloud must advertise the kernel that will receive offline ledgers: $text');
81
- final capabilities = (jsonDecode(text) as Map).cast<String, Object?>();
82
- final production =
83
- (capabilities['prod'] as Map?)?.cast<String, Object?>() ?? const {};
84
- final features = ((production['features'] as List?) ?? const [])
85
- .map((feature) => feature.toString())
86
- .toSet();
87
- expect(
88
- features,
89
- containsAll(<String>[
90
- 'conflictSnapshots',
91
- 'ownedAudience',
92
- ]),
93
- reason:
94
- 'the receiving kernel must support the law we are about to sync');
95
- } finally {
96
- capabilitiesHttp.close(force: true);
97
- }
98
-
99
- harness = await NomosTestHarness.cloudBound(
100
- cloud: cloud,
101
- deployJsonPaths: [_deploy],
102
- platformWorkspace: 'co2-product-platform',
103
- subject: _subject,
104
- nodeEntry: _runner,
105
- onDiagnostic: diagnostic,
106
- phaseTimeout: const Duration(seconds: 180),
107
- );
108
- var home = harness.homeSession;
109
- expect((await cloud.remoteHead(home.workspace)).initialized, isTrue,
110
- reason:
111
- 'the application born the identity home before business code ran');
112
- // The shipped Flutter scope starts this automatically. The headless acceptance test calls the same
113
- // client-owned lifecycle explicitly because it has no widget scope; no later sync/converge call is
114
- // permitted. The 60s fallback remains deliberately large: recovery must come from retry/reconnect,
115
- // not a test-only tight poll.
116
- await home.autoSync(fallbackMs: 60000);
117
-
118
- cloud.setOnline(false);
119
-
120
- // This is the whole tenant journey. Every argument is a business fact; Nomos owns the offline birth,
121
- // child acquisition, authorship, persistence, routing, retry and eventual parent-first custody.
122
- final homeClient = await Co2Client.bind(home, actor: _subject);
123
- final estateRef = await homeClient.birth(NomosBirthEstatePayload(
124
- reference: 'E-001',
125
- name: 'Foundry',
126
- country: 'GB',
127
- baselineTonnesCo2e: 4200,
128
- ));
129
- var estate = await harness.bridge.follow(
130
- estateRef,
131
- authorSecret: harness.deviceSecret(home.workspace),
132
- );
133
- await estate.autoSync(fallbackMs: 60000);
134
- final estateClient = await Co2Client.bind(estate, actor: _subject);
135
- final site = await estateClient.addSite(
136
- AddSitePayload(estateId: estateRef.id, name: 'Trafford Depot'),
137
- );
138
- final siteId = site.created.single.id;
139
- await estateClient.addAsset(AddAssetPayload(
140
- siteId: siteId,
141
- name: 'Main electricity meter',
142
- operationalStatus: 'commissioned',
143
- ));
144
- final localAssets = await estateClient.assetsBySite(siteId: siteId);
145
- expect(localAssets.single.name, 'Main electricity meter');
146
- expect(localAssets.single.operationalStatus?.valueOrNull, 'commissioned');
147
-
148
- expect((await cloud.remoteHead(estateRef.workspace)).initialized, isFalse,
149
- reason:
150
- 'the Estate remained fully usable without remote custody during the partition');
151
-
152
- // Kill the device process while both parent and child still carry unsynchronised local custody.
153
- // Restart is one framework verb: the product test supplies no snapshots, credentials, routes, retry
154
- // loop or topology ordering. Generated business reads must work before connectivity returns.
155
- await harness.restart();
156
- home = harness.homeSession;
157
- estate = harness.session(estateRef.workspace);
158
- final restartedClient = await Co2Client.bind(estate, actor: _subject);
159
- final restartedAssets =
160
- await restartedClient.assetsBySite(siteId: siteId);
161
- expect(restartedAssets.single.name, 'Main electricity meter');
162
- expect(restartedAssets.single.operationalStatus?.valueOrNull,
163
- 'commissioned');
164
- expect((await estate.verifyChain())['valid'], isTrue,
165
- reason:
166
- 'the unsynchronised Estate must replay locally before any network recovery');
167
-
168
- cloud.setOnline(true);
169
- // No tenant sync(), no harness.converge(), no topology ordering. The two stateful client sessions must
170
- // retain their local branches, retry after reconnect, establish the parent birth first as required by
171
- // the law, and finish child custody themselves.
172
- await _eventually('client-owned parent and child convergence', () async {
173
- final homeRemote = await cloud.remoteHead(home.workspace);
174
- final estateRemote = await cloud.remoteHead(estateRef.workspace);
175
- final homeMetrics = await home.metrics();
176
- final estateMetrics = await estate.metrics();
177
- final homeAhead = (homeMetrics['ahead'] as num?)?.toInt() ?? -1;
178
- final estateAhead = (estateMetrics['ahead'] as num?)?.toInt() ?? -1;
179
- if (homeRemote.initialized &&
180
- estateRemote.initialized &&
181
- homeAhead == 0 &&
182
- estateAhead == 0) {
183
- return true;
184
- }
185
- return null;
186
- }, timeout: const Duration(seconds: 180));
187
-
188
- final http = HttpClient();
189
- try {
190
- final request = await http.getUrl(Uri.parse(
191
- '${cloud.origin}/v2/workspaces/${estateRef.workspace}/assetsBySite?siteId=${Uri.encodeQueryComponent(siteId)}',
192
- ));
193
- final response = await request.close();
194
- final body =
195
- (jsonDecode(await response.transform(utf8.decoder).join()) as Map)
196
- .cast<String, Object?>();
197
- final rows = (body['rows'] as List?) ?? const [];
198
- expect(rows, hasLength(1));
199
- expect(((rows.single as Map)['data'] as Map)['name'],
200
- 'Main electricity meter');
201
- } finally {
202
- http.close(force: true);
203
- }
204
-
205
- final remoteEstate = await cloud.remoteHead(estateRef.workspace);
206
- expect(remoteEstate.initialized, isTrue);
207
- expect(remoteEstate.commits, greaterThan(0));
208
-
209
- // The serving cloud is not the proof of replayability. Start a fresh runner with no resident custody,
210
- // clone the Estate from remote custody, and re-fire the complete authority/law chain locally.
211
- final coldTransport = await NodeRunnerTransport.start(
212
- entry: _runner,
213
- environment: cloud.testKRootPublic == null
214
- ? null
215
- : {
216
- ...Platform.environment,
217
- 'NOMOS_TEST_K_ROOT_PUBLIC': cloud.testKRootPublic!,
218
- },
219
- );
220
- final cold = NomosBridge(coldTransport);
221
- try {
222
- await cold.ready;
223
- await cold.connect(
224
- cloud: cloud.origin,
225
- workspace: estateRef.workspace,
226
- clientId: 'co2-product-cold-peer',
227
- );
228
- final coldSession = cold.session(estateRef.workspace);
229
- final coldClient = await Co2Client.bind(coldSession, actor: _subject);
230
- expect((await coldClient.assetsBySite(siteId: siteId)).single.name,
231
- 'Main electricity meter');
232
- expect((await coldSession.verifyChain())['valid'], isTrue);
233
- } finally {
234
- await cold.dispose();
235
- await coldTransport.dispose();
236
- }
237
- } finally {
238
- try {
239
- await harness?.dispose();
240
- } catch (_) {}
241
- await cloud.stop();
242
- }
243
- }, timeout: const Timeout(Duration(minutes: 7)));
244
- }