@githolon/dsl 0.100.5 → 0.101.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 (97) hide show
  1. package/dart/CHANGELOG.md +12 -0
  2. package/dart/lib/src/dev_cloud.dart +46 -15
  3. package/dart/lib/src/test_harness.dart +220 -71
  4. package/dart/lib/src/webview_bridge.dart +640 -126
  5. package/dart/pubspec.lock +1 -1
  6. package/dart/pubspec.yaml +2 -2
  7. package/dart/test/acquire_session_credentials_test.dart +1 -1
  8. package/dart/test/adv_realm_interference_test.dart +1 -1
  9. package/dart/test/bundle_version_pairing_test.dart +11 -22
  10. package/dart/test/cluster_attested_test.dart +1 -1
  11. package/dart/test/cluster_births_test.dart +1 -1
  12. package/dart/test/cluster_device_keyed_acceptance_test.dart +1 -1
  13. package/dart/test/cluster_local_autosync_test.dart +1 -1
  14. package/dart/test/cluster_scale_test.dart +1 -1
  15. package/dart/test/co2_product_gauntlet_test.dart +228 -0
  16. package/dart/test/dlq_session_surface_test.dart +1 -1
  17. package/dart/test/errors_test.dart +63 -12
  18. package/dart/test/first_app_gauntlet_test.dart +132 -87
  19. package/dart/test/local_sync_inert_test.dart +10 -5
  20. package/dart/test/parent_signer_recovery_test.dart +1 -1
  21. package/dart/test/sealing_client_test.dart +1 -1
  22. package/dart/test/session_bound_test.dart +1 -1
  23. package/dart/test/test_harness_test.dart +2 -2
  24. package/dart/test/vo_read_typing_test.dart +1 -1
  25. package/dart/test/watch_after_offer_stall_test.dart +1 -1
  26. package/dart/test/webview_bridge_test.dart +274 -0
  27. package/dart_types/CHANGELOG.md +6 -0
  28. package/dart_types/lib/src/errors.dart +136 -30
  29. package/dart_types/lib/src/surfaced.dart +48 -2
  30. package/dart_types/lib/src/wire.dart +3 -1
  31. package/dart_types/pubspec.yaml +1 -1
  32. package/dart_types/test/surfaced_test.dart +19 -0
  33. package/package.json +1 -1
  34. package/src/activity.ts +43 -4
  35. package/src/aggregate.ts +132 -22
  36. package/src/attested_core.ts +1 -1
  37. package/src/authoring.ts +119 -6
  38. package/src/birth_meter.ts +4 -4
  39. package/src/birth_meter_ensure.ts +165 -29
  40. package/src/births.ts +53 -22
  41. package/src/build_package.ts +96 -20
  42. package/src/business_birth.ts +79 -16
  43. package/src/codegen_dart.ts +267 -37
  44. package/src/codegen_dart_client.ts +930 -87
  45. package/src/codegen_openapi.ts +28 -5
  46. package/src/codegen_proof.ts +107 -11
  47. package/src/codegen_ts.ts +961 -135
  48. package/src/compile_package_main.ts +172 -53
  49. package/src/conflicts.ts +252 -0
  50. package/src/describe_model.ts +448 -0
  51. package/src/directive.ts +5 -38
  52. package/src/drivers.ts +12 -4
  53. package/src/engine_entry.ts +4 -0
  54. package/src/evolve_preview.ts +72 -11
  55. package/src/fields.ts +201 -55
  56. package/src/framework/application_home.ts +103 -0
  57. package/src/framework/application_home_domain.ts +2 -0
  58. package/src/framework/attested_receipts.ts +2 -2
  59. package/src/framework/auth_provider_law.ts +51 -24
  60. package/src/framework/business_corrections.ts +170 -0
  61. package/src/framework/disclosure.ts +1 -1
  62. package/src/framework/domain_lifecycle.ts +250 -16
  63. package/src/framework/encryption_identity.ts +1 -1
  64. package/src/framework/encryption_scope.ts +21 -13
  65. package/src/framework/era_certification.ts +1 -1
  66. package/src/framework/framework_aggregates.ts +2 -0
  67. package/src/framework/genesis.ts +1 -1
  68. package/src/framework/home.ts +22 -9
  69. package/src/framework/identity.ts +5 -5
  70. package/src/framework/impure_capability.ts +2 -2
  71. package/src/framework/owned_custody.ts +561 -0
  72. package/src/framework/platform_admin.ts +1 -1
  73. package/src/framework/relations.ts +9 -1
  74. package/src/framework/repair.ts +1 -1
  75. package/src/framework/signing_identity.ts +2 -2
  76. package/src/framework/source_roster.ts +1 -1
  77. package/src/framework/workspaces.ts +17 -10
  78. package/src/generated_client_names.ts +39 -0
  79. package/src/index.ts +20 -15
  80. package/src/kernel_capabilities.ts +25 -1
  81. package/src/law_check.ts +4 -1
  82. package/src/manifest.ts +356 -63
  83. package/src/read_projection_manifest.ts +8 -0
  84. package/src/read_projection_namespace.ts +4 -0
  85. package/src/relation.ts +1 -1
  86. package/src/shape_compat.ts +61 -5
  87. package/src/stable_ids.ts +38 -6
  88. package/src/stable_ids_types.ts +30 -0
  89. package/src/surfaced_lint.ts +0 -0
  90. package/src/usd.ts +62 -11
  91. package/src/usd_layers.ts +54 -3
  92. package/src/value_object.ts +23 -19
  93. package/src/wire.ts +1 -0
  94. package/src/wire_encode.ts +48 -4
  95. package/src/workspace_routing.ts +57 -0
  96. package/src/workspace_sharding.ts +9 -9
  97. package/src/certified_read.ts +0 -313
package/dart/pubspec.lock CHANGED
@@ -191,7 +191,7 @@ packages:
191
191
  path: "../dart_types"
192
192
  relative: true
193
193
  source: path
194
- version: "0.6.0"
194
+ version: "0.7.0"
195
195
  package_config:
196
196
  dependency: transitive
197
197
  description:
package/dart/pubspec.yaml CHANGED
@@ -11,7 +11,7 @@ description: >-
11
11
  Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
12
12
  code-generated per-domain aggregate + directive-payload types. (De)serializes
13
13
  the EXACT serde JSON the Rust kernel + projection produce/consume.
14
- version: 0.62.1
14
+ version: 0.63.0
15
15
 
16
16
  environment:
17
17
  sdk: ^3.5.0
@@ -20,7 +20,7 @@ dependencies:
20
20
  # The pure-data core (wire types, offer codec, NomosReads, schema validation, error
21
21
  # info). nomos_client depends on and re-exports it, so existing imports are unchanged.
22
22
  # For local monorepo dev, pubspec_overrides.yaml points this at ../dart_types.
23
- nomos_types: ^0.6.0
23
+ nomos_types: ^0.7.0
24
24
  json_schema: ^5.2.2
25
25
  crypto: ^3.0.0
26
26
 
@@ -29,7 +29,7 @@ String? _missingPrereq() {
29
29
  return 'missing fixture $_docsDeploy — compile it: '
30
30
  '(cd bench/scale/fixtures/surfaced && node ../../../../dsl/compile_package.mjs)';
31
31
  }
32
- if (!File('$_runtimeDir/holon.wasm').existsSync() ||
32
+ if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
33
33
  !File('$_runtimeDir/packages.json').existsSync()) {
34
34
  return 'warm ~/.holon/runtime first (run any `githolon` command once)';
35
35
  }
@@ -37,7 +37,7 @@ String? _missingPrereq() {
37
37
  for (final p in [_repoEntry, _installer, _guestbook, _surfaced, _attributed, _estateBirth]) {
38
38
  if (!File(p).existsSync()) return 'missing $p (compile the fixture / example first)';
39
39
  }
40
- if (!File('$_runtimeDir/holon.wasm').existsSync() ||
40
+ if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
41
41
  !File('$_runtimeDir/packages.json').existsSync()) {
42
42
  return 'warm ~/.holon/runtime first (run any `githolon` command once)';
43
43
  }
@@ -1,7 +1,6 @@
1
- // THE VERSION-PAIRING TRIPWIRE (the pubspec-pins pattern): kExpectedRunnerBundleVersion must equal
2
- // the @githolon/client version this repo is about to release (cloud/web-client/package.json) — a
3
- // client release without the Dart floor bump (or vice versa) fails the suite, so the stale-bundle
4
- // check can neither lag (missing real staleness) nor lead (refusing the current bundle).
1
+ // THE VERSION-FLOOR TRIPWIRE: the @githolon/client built by this checkout must satisfy the minimum
2
+ // runner version declared by nomos_client. Changesets may advance the npm package metadata without
3
+ // changing its protocol, so a newer runner is valid; an older runner than the Dart floor is not.
5
4
  import 'dart:convert';
6
5
  import 'dart:io';
7
6
 
@@ -16,25 +15,15 @@ void main() {
16
15
  return;
17
16
  }
18
17
  final v = (jsonDecode(f.readAsStringSync()) as Map)['version'] as String;
19
- if (kExpectedRunnerBundleVersion == v) return; // released state: exact pair
20
- // PENDING-RELEASE state: the floor may lead package.json by exactly the bump a queued
21
- // changeset will apply (the changesets flow bumps package.json only when the version PR
22
- // merges). Anything else — lagging, or leading with no changeset queued — is drift.
23
- final changesets = Directory('${Directory.current.path}/../../.changeset')
24
- .listSync()
25
- .whereType<File>()
26
- .where((c) => c.path.endsWith('.md') && !c.path.endsWith('README.md'))
27
- .where((c) => c.readAsStringSync().contains('@githolon/client'));
28
- expect(changesets, isNotEmpty,
29
- reason: 'kExpectedRunnerBundleVersion ($kExpectedRunnerBundleVersion) != @githolon/client '
30
- '($v) and NO client changeset is queued — bump the floor WITH the client release '
31
- '(the stale-bundle floor must move with the bundle it ships beside)');
32
18
  List<int> parse(String x) =>
33
19
  x.split('.').map((p) => int.tryParse(p) ?? 0).toList();
34
- final a = parse(kExpectedRunnerBundleVersion), b = parse(v);
35
- final leads = a[0] > b[0] ||
36
- (a[0] == b[0] && (a[1] > b[1] || (a[1] == b[1] && a[2] > b[2])));
37
- expect(leads, isTrue,
38
- reason: 'the floor must LEAD (never lag) package.json while a release is queued');
20
+ final floor = parse(kExpectedRunnerBundleVersion), runner = parse(v);
21
+ final runnerMeetsFloor = runner[0] > floor[0] ||
22
+ (runner[0] == floor[0] &&
23
+ (runner[1] > floor[1] ||
24
+ (runner[1] == floor[1] && runner[2] >= floor[2])));
25
+ expect(runnerMeetsFloor, isTrue,
26
+ reason: '@githolon/client $v is older than nomos_client runner floor '
27
+ '$kExpectedRunnerBundleVersion');
39
28
  });
40
29
  }
@@ -42,7 +42,7 @@ String? _missingPrereq() {
42
42
  '(needs `dart: { support: "package" }` in nomos.package.mjs) — missing $p';
43
43
  }
44
44
  }
45
- if (!File('$_runtimeDir/holon.wasm').existsSync() ||
45
+ if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
46
46
  !File('$_runtimeDir/packages.json').existsSync()) {
47
47
  return 'warm ~/.holon/runtime first (run any `githolon` command once)';
48
48
  }
@@ -37,7 +37,7 @@ String? _missingPrereq() {
37
37
  if (!File(_birthDeploy).existsSync()) {
38
38
  return 'compile the fixture: cd bench/scale/fixtures/estate-birth && node ../../../../dsl/compile_package.mjs — missing $_birthDeploy';
39
39
  }
40
- if (!File('$_runtimeDir/holon.wasm').existsSync() ||
40
+ if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
41
41
  !File('$_runtimeDir/packages.json').existsSync()) {
42
42
  return 'warm ~/.holon/runtime first (run any `githolon` command once)';
43
43
  }
@@ -27,7 +27,7 @@ String? _missingPrereq() {
27
27
  for (final p in [_catDeploy, _estateDeploy]) {
28
28
  if (!File(p).existsSync()) return 'missing fixture $p (compile bench/scale/fixtures first)';
29
29
  }
30
- if (!File('$_runtimeDir/holon.wasm').existsSync() ||
30
+ if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
31
31
  !File('$_runtimeDir/packages.json').existsSync()) {
32
32
  return 'warm ~/.holon/runtime first (run any `githolon` command once)';
33
33
  }
@@ -24,7 +24,7 @@ String? _missingPrereq() {
24
24
  if (!File(_guestbookDeploy).existsSync()) {
25
25
  return 'compile the guestbook example: cd examples/guestbook && npx nomos-compile — missing $_guestbookDeploy';
26
26
  }
27
- if (!File('$_runtimeDir/holon.wasm').existsSync() ||
27
+ if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
28
28
  !File('$_runtimeDir/packages.json').existsSync()) {
29
29
  return 'warm ~/.holon/runtime first (run any `githolon` command once)';
30
30
  }
@@ -28,7 +28,7 @@ String? _missingPrereq() {
28
28
  if (!File(_guestbookDeploy).existsSync()) {
29
29
  return 'compile the guestbook example: cd examples/guestbook && npx nomos-compile — missing $_guestbookDeploy';
30
30
  }
31
- if (!File('$_runtimeDir/holon.wasm').existsSync() ||
31
+ if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
32
32
  !File('$_runtimeDir/packages.json').existsSync()) {
33
33
  return 'warm ~/.holon/runtime first (run any `githolon` command once)';
34
34
  }
@@ -0,0 +1,228 @@
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
+ final 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
+ final 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
+ cloud.setOnline(true);
153
+ // No tenant sync(), no harness.converge(), no topology ordering. The two stateful client sessions must
154
+ // retain their local branches, retry after reconnect, establish the parent birth first as required by
155
+ // the law, and finish child custody themselves.
156
+ await _eventually('client-owned parent and child convergence', () async {
157
+ final homeRemote = await cloud.remoteHead(home.workspace);
158
+ final estateRemote = await cloud.remoteHead(estateRef.workspace);
159
+ final homeMetrics = await home.metrics();
160
+ final estateMetrics = await estate.metrics();
161
+ final homeAhead = (homeMetrics['ahead'] as num?)?.toInt() ?? -1;
162
+ final estateAhead = (estateMetrics['ahead'] as num?)?.toInt() ?? -1;
163
+ if (homeRemote.initialized &&
164
+ estateRemote.initialized &&
165
+ homeAhead == 0 &&
166
+ estateAhead == 0) {
167
+ return true;
168
+ }
169
+ return null;
170
+ }, timeout: const Duration(seconds: 180));
171
+
172
+ final http = HttpClient();
173
+ try {
174
+ final request = await http.getUrl(Uri.parse(
175
+ '${cloud.origin}/v2/workspaces/${estateRef.workspace}/assetsBySite?siteId=${Uri.encodeQueryComponent(siteId)}',
176
+ ));
177
+ final response = await request.close();
178
+ final body =
179
+ (jsonDecode(await response.transform(utf8.decoder).join()) as Map)
180
+ .cast<String, Object?>();
181
+ final rows = (body['rows'] as List?) ?? const [];
182
+ expect(rows, hasLength(1));
183
+ expect(((rows.single as Map)['data'] as Map)['name'],
184
+ 'Main electricity meter');
185
+ } finally {
186
+ http.close(force: true);
187
+ }
188
+
189
+ final remoteEstate = await cloud.remoteHead(estateRef.workspace);
190
+ expect(remoteEstate.initialized, isTrue);
191
+ expect(remoteEstate.commits, greaterThan(0));
192
+
193
+ // The serving cloud is not the proof of replayability. Start a fresh runner with no resident custody,
194
+ // clone the Estate from remote custody, and re-fire the complete authority/law chain locally.
195
+ final coldTransport = await NodeRunnerTransport.start(
196
+ entry: _runner,
197
+ environment: cloud.testKRootPublic == null
198
+ ? null
199
+ : {
200
+ ...Platform.environment,
201
+ 'NOMOS_TEST_K_ROOT_PUBLIC': cloud.testKRootPublic!,
202
+ },
203
+ );
204
+ final cold = NomosBridge(coldTransport);
205
+ try {
206
+ await cold.ready;
207
+ await cold.connect(
208
+ cloud: cloud.origin,
209
+ workspace: estateRef.workspace,
210
+ clientId: 'co2-product-cold-peer',
211
+ );
212
+ final coldSession = cold.session(estateRef.workspace);
213
+ final coldClient = await Co2Client.bind(coldSession, actor: _subject);
214
+ expect((await coldClient.assetsBySite(siteId: siteId)).single.name,
215
+ 'Main electricity meter');
216
+ expect((await coldSession.verifyChain())['valid'], isTrue);
217
+ } finally {
218
+ await cold.dispose();
219
+ await coldTransport.dispose();
220
+ }
221
+ } finally {
222
+ try {
223
+ await harness?.dispose();
224
+ } catch (_) {}
225
+ await cloud.stop();
226
+ }
227
+ }, timeout: const Timeout(Duration(minutes: 7)));
228
+ }
@@ -36,7 +36,7 @@ String? _missingPrereq() {
36
36
  return 'missing fixture ${e.key} — compile it: (cd ${e.value} && node ../../../../dsl/compile_package.mjs)';
37
37
  }
38
38
  }
39
- if (!File('$_runtimeDir/holon.wasm').existsSync() ||
39
+ if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
40
40
  !File('$_runtimeDir/packages.json').existsSync()) {
41
41
  return 'warm ~/.holon/runtime first (run any `githolon` command once)';
42
42
  }
@@ -4,11 +4,45 @@
4
4
  import 'package:nomos_client/nomos_client.dart';
5
5
  import 'package:test/test.dart';
6
6
 
7
- NomosErrorInfo info(String code, {Map<String, Object?> attrs = const {}, String? workspace, String? operation}) =>
8
- NomosErrorInfo(code: code, message: 'refused', workspace: workspace, operation: operation, attributes: attrs);
7
+ NomosErrorInfo info(String code,
8
+ {Map<String, Object?> attrs = const {},
9
+ String? workspace,
10
+ String? operation}) =>
11
+ NomosErrorInfo(
12
+ code: code,
13
+ message: 'refused',
14
+ workspace: workspace,
15
+ operation: operation,
16
+ attributes: attrs);
9
17
 
10
18
  void main() {
11
- test('law.* mints LawMismatchError with both sides + compat verdict + the exact message', () {
19
+ test(
20
+ 'owned recovery mints one typed resumable state with the private protocol hidden',
21
+ () {
22
+ final e = nomosErrorFromInfo(info(
23
+ 'nomos.owned.recovery_pending',
24
+ workspace: 'home-ada',
25
+ attrs: {
26
+ 'nomos.owned.stage': 'awaitingRecoveryPrincipal',
27
+ 'nomos.owned.orderId': 'NomosOwnedRecoveryOrder_1',
28
+ 'nomos.owned.recoveryPrincipal': 'user:bob',
29
+ 'nomos.owned.recommendedAction':
30
+ NomosRecommendedAction.openRecoveryPrincipalDevice,
31
+ },
32
+ ));
33
+ expect(e, isA<OwnedRecoveryPending>());
34
+ final recovery = e as OwnedRecoveryPending;
35
+ expect(recovery.workspace, 'home-ada');
36
+ expect(recovery.stage, 'awaitingRecoveryPrincipal');
37
+ expect(recovery.orderId, 'NomosOwnedRecoveryOrder_1');
38
+ expect(recovery.recoveryPrincipal, 'user:bob');
39
+ expect(recovery.recommendedAction,
40
+ NomosRecommendedAction.openRecoveryPrincipalDevice);
41
+ });
42
+
43
+ test(
44
+ 'law.* mints LawMismatchError with both sides + compat verdict + the exact message',
45
+ () {
12
46
  final e = nomosErrorFromInfo(info(
13
47
  'nomos.law.mismatch',
14
48
  workspace: 'ws-1',
@@ -44,11 +78,15 @@ void main() {
44
78
  'nomos.law.workspaceRuns': 'bbbb',
45
79
  'nomos.law.structurallyCompatible': true,
46
80
  })) as LawMismatchError;
47
- expect(m.mismatchMessage, startsWith('client generated for law aaaa, workspace runs law bbbb; structurally compatible — proceeding —'));
81
+ expect(
82
+ m.mismatchMessage,
83
+ startsWith(
84
+ 'client generated for law aaaa, workspace runs law bbbb; structurally compatible — proceeding —'));
48
85
  });
49
86
 
50
87
  test('auth.* mints AuthError with reauth as the remedy', () {
51
- final e = nomosErrorFromInfo(info('nomos.auth.issuer_mismatch', workspace: 'ws-1', attrs: {'nomos.auth.issuer': 'https://evil'}));
88
+ final e = nomosErrorFromInfo(info('nomos.auth.issuer_mismatch',
89
+ workspace: 'ws-1', attrs: {'nomos.auth.issuer': 'https://evil'}));
52
90
  expect(e, isA<AuthError>());
53
91
  final a = e as AuthError;
54
92
  expect(a.code, 'nomos.auth.issuer_mismatch');
@@ -68,7 +106,9 @@ void main() {
68
106
  expect(l.recommendedAction, NomosRecommendedAction.retry);
69
107
  });
70
108
 
71
- test('authz signer_enrollment_missing mints ParentSignerEnrollmentMissing with the ONE remedy', () {
109
+ test(
110
+ 'authz signer_enrollment_missing mints ParentSignerEnrollmentMissing with the ONE remedy',
111
+ () {
72
112
  final e = nomosErrorFromInfo(info(
73
113
  'nomos.authz.signer_enrollment_missing',
74
114
  workspace: 'co2-platform',
@@ -90,18 +130,26 @@ void main() {
90
130
  expect(p.object, 'identity:u-1');
91
131
  expect(p.relation, 'signer');
92
132
  expect(p.directiveId, 'birthEstateWorkspace');
93
- expect(p.recommendedAction, NomosRecommendedAction.ensureParentSignerEnrollment);
133
+ expect(p.recommendedAction,
134
+ NomosRecommendedAction.ensureParentSignerEnrollment);
94
135
  });
95
136
 
96
- test('authz cause=missing-signer-enrollment routes to ParentSignerEnrollmentMissing even under the generic code', () {
137
+ test(
138
+ 'authz cause=missing-signer-enrollment routes to ParentSignerEnrollmentMissing even under the generic code',
139
+ () {
97
140
  final e = nomosErrorFromInfo(info(
98
141
  'nomos.authz.refused',
99
- attrs: {'nomos.authz.cause': 'missing-signer-enrollment', 'nomos.authz.actor': 'user:u-9'},
142
+ attrs: {
143
+ 'nomos.authz.cause': 'missing-signer-enrollment',
144
+ 'nomos.authz.actor': 'user:u-9'
145
+ },
100
146
  ));
101
147
  expect(e, isA<ParentSignerEnrollmentMissing>());
102
148
  });
103
149
 
104
- test('authz missing_role stays AuthzRefusedError — role-absent is DISTINGUISHED from signer-absent', () {
150
+ test(
151
+ 'authz missing_role stays AuthzRefusedError — role-absent is DISTINGUISHED from signer-absent',
152
+ () {
105
153
  final e = nomosErrorFromInfo(info(
106
154
  'nomos.authz.missing_role',
107
155
  workspace: 'ws-1',
@@ -120,8 +168,11 @@ void main() {
120
168
  expect(a.object, 'workspace:self');
121
169
  });
122
170
 
123
- test('custody.* still mints CustodyError (unchanged), unknown codes stay generic', () {
124
- expect(nomosErrorFromInfo(info('nomos.custody.missing_object')), isA<CustodyError>());
171
+ test(
172
+ 'custody.* still mints CustodyError (unchanged), unknown codes stay generic',
173
+ () {
174
+ expect(nomosErrorFromInfo(info('nomos.custody.missing_object')),
175
+ isA<CustodyError>());
125
176
  final g = nomosErrorFromInfo(info('nomos.bridge.error'));
126
177
  expect(g, isA<NomosBridgeError>());
127
178
  expect(g, isNot(isA<LawMismatchError>()));