@githolon/dsl 0.103.3 → 0.103.5

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 (42) hide show
  1. package/dart/CHANGELOG.md +15 -0
  2. package/dart/lib/src/runner/node-entry.mjs +411 -149
  3. package/dart/lib/src/runner/offline-install.mjs +222 -99
  4. package/dart/lib/src/session_bound.dart +111 -2
  5. package/dart/lib/src/test_harness.dart +117 -66
  6. package/dart/lib/src/webview_bridge.dart +726 -61
  7. package/dart/pubspec.lock +1 -1
  8. package/dart/pubspec.yaml +4 -5
  9. package/dart/test/acquire_session_test.dart +62 -5
  10. package/dart/test/cloud_bound_topology_test.dart +59 -38
  11. package/dart/test/session_bound_test.dart +25 -10
  12. package/dart/test/webview_bridge_test.dart +357 -9
  13. package/dart_types/CHANGELOG.md +5 -0
  14. package/dart_types/lib/nomos_types.dart +1 -0
  15. package/dart_types/lib/src/git_object_id.dart +37 -0
  16. package/dart_types/pubspec.yaml +1 -1
  17. package/package.json +1 -1
  18. package/runtime/generated-application-host.js +425 -145
  19. package/src/births.ts +16 -1
  20. package/src/business_birth.ts +67 -46
  21. package/src/codegen_dart.ts +68 -12
  22. package/src/codegen_dart_application.ts +114 -19
  23. package/src/codegen_dart_client.ts +149 -6
  24. package/src/codegen_proof.ts +16 -9
  25. package/src/codegen_provider_dart.ts +5 -0
  26. package/src/codegen_ts.ts +115 -19
  27. package/src/compile_package_main.ts +142 -40
  28. package/src/fields.ts +43 -0
  29. package/src/framework/attested_receipts.ts +34 -1
  30. package/src/framework/domain_lifecycle.ts +140 -83
  31. package/src/framework/genesis.ts +2 -1
  32. package/src/framework/home.ts +20 -45
  33. package/src/framework/workspaces.ts +62 -29
  34. package/src/idof_lint.ts +65 -51
  35. package/src/index.ts +21 -1
  36. package/src/intent.ts +11 -1
  37. package/src/law_graph.ts +81 -11
  38. package/src/law_graph_usda.ts +45 -54
  39. package/src/relation_schema.ts +18 -0
  40. package/src/shape_compat.ts +3 -0
  41. package/src/stable_ids_for_module.ts +7 -1
  42. package/src/value_object.ts +40 -0
@@ -134,6 +134,11 @@ class DomainClientSessionMismatch implements Exception {
134
134
  required this.hint,
135
135
  });
136
136
 
137
+ /// True only for the one bind failure a staged generated successor can
138
+ /// lawfully resolve by proving itself against this workspace's history.
139
+ bool get canAttemptGeneratedModelAdoption =>
140
+ cause == SessionMismatchCause.clientExpectsNewerInterface;
141
+
137
142
  @override
138
143
  String toString() =>
139
144
  'DomainClientSessionMismatch: session "${sessionId ?? '(unbound)'}" (workspace '
@@ -145,6 +150,93 @@ class DomainClientSessionMismatch implements Exception {
145
150
  'active law: ${activeDomainHashes.isEmpty ? '(unknown)' : activeDomainHashes.join(', ')})';
146
151
  }
147
152
 
153
+ /// A generated candidate reached the lawful model-adoption boundary while
154
+ /// binding, but exact-history proof requires a deliberate business decision or
155
+ /// could not complete. The old model remains active and no compatibility
156
+ /// fallback is used.
157
+ enum DomainModelDifferenceKind { rules, outcome, evidence }
158
+
159
+ final class DomainModelDifference {
160
+ final DomainModelDifferenceKind kind;
161
+ final String? area;
162
+ final String? action;
163
+ final String summary;
164
+
165
+ const DomainModelDifference({
166
+ required this.kind,
167
+ this.area,
168
+ this.action,
169
+ required this.summary,
170
+ });
171
+ }
172
+
173
+ final class DomainModelReview {
174
+ final int checkedActions;
175
+ final int candidateActions;
176
+ final bool? sameOutcome;
177
+ final List<DomainModelDifference> differences;
178
+
179
+ const DomainModelReview({
180
+ required this.checkedActions,
181
+ required this.candidateActions,
182
+ required this.sameOutcome,
183
+ required this.differences,
184
+ });
185
+ }
186
+
187
+ class DomainClientModelAdoptionRequired implements Exception {
188
+ final String expectedDomainKey;
189
+ final String expectedDomainHash;
190
+ final String status;
191
+ final String summary;
192
+ final DomainModelReview review;
193
+ final Future<void> Function()? _stage;
194
+ final Future<void> Function(String reason)? _accept;
195
+
196
+ const DomainClientModelAdoptionRequired({
197
+ required this.expectedDomainKey,
198
+ required this.expectedDomainHash,
199
+ required this.status,
200
+ required this.summary,
201
+ required this.review,
202
+ Future<void> Function()? stage,
203
+ Future<void> Function(String reason)? accept,
204
+ }) : _stage = stage,
205
+ _accept = accept;
206
+
207
+ bool get canStage => _stage != null;
208
+
209
+ Future<void> stage() {
210
+ final stage = _stage;
211
+ if (stage == null) {
212
+ throw StateError('This generated model is not waiting to be staged.');
213
+ }
214
+ return stage();
215
+ }
216
+
217
+ bool get canAccept => _accept != null;
218
+
219
+ Future<void> accept(String reason) {
220
+ final value = reason.trim();
221
+ if (value.isEmpty) {
222
+ throw ArgumentError.value(
223
+ reason,
224
+ 'reason',
225
+ 'must explain why changing prior business outcomes is intended',
226
+ );
227
+ }
228
+ final accept = _accept;
229
+ if (accept == null) {
230
+ throw StateError('This generated model change cannot be accepted.');
231
+ }
232
+ return accept(value);
233
+ }
234
+
235
+ @override
236
+ String toString() => 'DomainClientModelAdoptionRequired: generated model for '
237
+ '"$expectedDomainKey" ($expectedDomainHash) is $status. $summary';
238
+ }
239
+
148
240
  /// The context-rich replacement for the kernel's generic
149
241
  /// `no active installed law declares domain 'x' … deploy the domain package first` refusal —
150
242
  /// thrown wherever that error used to surface (bridge dispatch, birth lanes) so an app dev sees
@@ -287,11 +379,17 @@ extension NomosSessionCompat on NomosSession {
287
379
  // 4. CLASSIFY the four causes (ask 9).
288
380
  SessionMismatchCause cause;
289
381
  String hint;
290
- if (incompatible.isNotEmpty) {
382
+ if (nmsInternalClientExpectsNewerInterface(
383
+ domain: iface.domain,
384
+ activeDomainKeys: activeKeys,
385
+ missing: missing,
386
+ incompatible: incompatible,
387
+ )) {
291
388
  cause = SessionMismatchCause.clientExpectsNewerInterface;
389
+ final differences = [...incompatible, ...missing];
292
390
  hint =
293
391
  'the domain is installed but type-divergent from what this client requires '
294
- '(${incompatible.join('; ')}) — recompile the client against the deployed law, or evolve the law';
392
+ '(${differences.join('; ')}) — recompile the client against the deployed law, or evolve the law';
295
393
  } else if (activeKeys.isNotEmpty && !activeKeys.contains(iface.domain)) {
296
394
  cause = SessionMismatchCause.wrongPackageMounted;
297
395
  hint =
@@ -334,3 +432,14 @@ extension NomosSessionCompat on NomosSession {
334
432
  );
335
433
  }
336
434
  }
435
+
436
+ /// Generator/runtime classification seam. A required member missing from an
437
+ /// already installed domain is a newer-client shape, not an absent deployment.
438
+ bool nmsInternalClientExpectsNewerInterface({
439
+ required String domain,
440
+ required List<String> activeDomainKeys,
441
+ required List<String> missing,
442
+ required List<String> incompatible,
443
+ }) =>
444
+ incompatible.isNotEmpty ||
445
+ (missing.isNotEmpty && activeDomainKeys.contains(domain));
@@ -46,7 +46,25 @@ import 'package:package_config/package_config.dart' show loadPackageConfigUri;
46
46
  import 'dev_cloud.dart';
47
47
  import 'node_runner_transport.dart';
48
48
  import 'session_bound.dart' show DomainNotInstalledForSession;
49
- import 'webview_bridge.dart' show NomosBridge, NomosSession;
49
+ import 'webview_bridge.dart'
50
+ show NomosBirthCertificateSpec, NomosBridge, NomosSession;
51
+
52
+ enum _KernelRuntimeArtifact {
53
+ executable('holon.wasm'),
54
+ stageZeroLaw('bootstrap.law.usdz'),
55
+ controllerLaw('nomos.law.usdz');
56
+
57
+ const _KernelRuntimeArtifact(this.fileName);
58
+ final String fileName;
59
+ }
60
+
61
+ enum _CandidateKernelBinary {
62
+ production('holon.wasm'),
63
+ testMint('holon-testmint.wasm');
64
+
65
+ const _CandidateKernelBinary(this.fileName);
66
+ final String fileName;
67
+ }
50
68
 
51
69
  /// A source checkout is itself a coordinated release candidate: compiler, framework packages and kernels
52
70
  /// move together. Generated scenarios must not combine those source-generated laws with a recently-written
@@ -697,7 +715,6 @@ class NomosTestHarness {
697
715
  'the offline application parent did not return the framework law, application law and test device needed to birth the signed-in application',
698
716
  );
699
717
  }
700
- final keyHash = sha256.convert(base64Decode(devicePublic)).toString();
701
718
  final bornAt = DateTime.now().toUtc().toIso8601String();
702
719
  await _phased(
703
720
  phase: 'application: establish the parent birth policy',
@@ -734,7 +751,6 @@ class NomosTestHarness {
734
751
  authorSecret: deviceSecret,
735
752
  payload: <String, Object?>{
736
753
  'owner': owner,
737
- 'ownerKeyHash': keyHash,
738
754
  'frameworkHash': framework,
739
755
  // Ordered application deployment is reference packages first, deferred child payload last.
740
756
  // The parent serves the references; the receiving group is born with only its payload law.
@@ -845,14 +861,16 @@ class NomosTestHarness {
845
861
  );
846
862
  },
847
863
  );
848
- final parentCert = await plane.signBirthCert(<String, Object?>{
849
- 'child': workspace,
850
- 'founder': owner,
851
- 'verified': true,
852
- 'parentKey': devicePublic,
853
- 'parentId': 'root',
854
- 'epoch': 0,
855
- }, deviceSecret);
864
+ final parentCert = await plane.signBirthCert(
865
+ NomosBirthCertificateSpec(
866
+ child: workspace,
867
+ founder: owner,
868
+ verified: true,
869
+ parentKey: devicePublic,
870
+ parentId: 'root',
871
+ epoch: 0,
872
+ ),
873
+ deviceSecret);
856
874
  await _phased(
857
875
  phase: 'owner: establish the verified test application root',
858
876
  workspace: workspace,
@@ -873,7 +891,7 @@ class NomosTestHarness {
873
891
  'parentKey': devicePublic,
874
892
  'epoch': 0,
875
893
  'certChain': jsonEncode(<String, Object?>{
876
- 'links': <Object?>[parentCert],
894
+ 'links': <Object?>[parentCert.toWire()],
877
895
  }),
878
896
  },
879
897
  );
@@ -1512,38 +1530,62 @@ class NomosTestHarness {
1512
1530
  }
1513
1531
  // 2/6 — the LOCAL engine plane, CLOUD-BOUND, spawned BEFORE the birth: the runner's local kernel is
1514
1532
  // what mints the device key + signs the birth cert (era-0: a birth is ATTESTED or not at all).
1515
- // The executable is device custody too. A process restart while offline must not fetch the kernel
1516
- // before it can replay the persisted ledger. Release CI supplies the exact candidate runtime. An
1517
- // ordinary local test pins the exact kernel advertised by its ephemeral cloud while still online;
1518
- // never fall back to a possibly stale global cache.
1533
+ // The executable AND its framework-law packages are one runtime bundle in device custody. A process
1534
+ // restart while offline must not fetch any member before it can replay the persisted ledger. Release CI
1535
+ // supplies the exact candidate bundle. An ordinary local test pins all three artifacts advertised by its
1536
+ // ephemeral cloud while still online; never combine a candidate kernel with stale framework law.
1519
1537
  final candidateRuntimeDir = Platform.environment['NOMOS_DART_RUNTIME'] ??
1520
1538
  Platform.environment['NOMOS_OFFLINE_RUNTIME'];
1521
- var localWasm = candidateRuntimeDir == null
1522
- ? ''
1523
- : '$candidateRuntimeDir/${cloud.testKRootPublic == null ? 'holon.wasm' : 'holon-testmint.wasm'}';
1524
- if (localWasm.isEmpty || !File(localWasm).existsSync()) {
1525
- localWasm = '${workdir.path}/client-kernel.wasm';
1539
+ final localRuntime = await Directory('${workdir.path}/client-runtime')
1540
+ .create(recursive: true);
1541
+ final selectedKernel = cloud.testKRootPublic == null
1542
+ ? _CandidateKernelBinary.production
1543
+ : _CandidateKernelBinary.testMint;
1544
+ final candidateArtifacts = candidateRuntimeDir == null
1545
+ ? const <_KernelRuntimeArtifact, String>{}
1546
+ : <_KernelRuntimeArtifact, String>{
1547
+ _KernelRuntimeArtifact.executable:
1548
+ '$candidateRuntimeDir/${selectedKernel.fileName}',
1549
+ _KernelRuntimeArtifact.stageZeroLaw:
1550
+ '$candidateRuntimeDir/${_KernelRuntimeArtifact.stageZeroLaw.fileName}',
1551
+ _KernelRuntimeArtifact.controllerLaw:
1552
+ '$candidateRuntimeDir/${_KernelRuntimeArtifact.controllerLaw.fileName}',
1553
+ };
1554
+ final hasCandidateBundle = candidateArtifacts.length ==
1555
+ _KernelRuntimeArtifact.values.length &&
1556
+ candidateArtifacts.values.every((path) => File(path).existsSync());
1557
+ if (hasCandidateBundle) {
1558
+ for (final artifact in candidateArtifacts.entries) {
1559
+ await File(artifact.value)
1560
+ .copy('${localRuntime.path}/${artifact.key.fileName}');
1561
+ }
1562
+ } else {
1526
1563
  await _phased(
1527
- phase: '2/6: pin the client kernel for offline restart',
1528
- awaiting: 'GET /v1/runtime/holon.wasm',
1564
+ phase: '2/6: pin the client runtime bundle for offline restart',
1565
+ awaiting: 'GET kernel + framework USDA packages',
1529
1566
  timeout: phaseTimeout ?? _kInstallDeadline,
1530
1567
  onDiagnostic: onDiagnostic,
1531
1568
  run: () async {
1532
- final request = await http.getUrl(
1533
- Uri.parse('${cloud.origin}/v1/runtime/holon.wasm'),
1534
- );
1535
- final response = await request.close();
1536
- final bytes = await response.fold<List<int>>(
1537
- <int>[],
1538
- (all, chunk) => all..addAll(chunk),
1539
- );
1540
- if (response.statusCode != HttpStatus.ok || bytes.length < 1024) {
1541
- throw NomosTestHarnessException(
1542
- 'cloudBound: could not pin the client kernel before going offline '
1543
- '(status ${response.statusCode}, ${bytes.length} bytes)',
1569
+ for (final artifact in _KernelRuntimeArtifact.values) {
1570
+ final request = await http.getUrl(
1571
+ Uri.parse(
1572
+ '${cloud.origin}/v1/runtime/${artifact.fileName}',
1573
+ ),
1544
1574
  );
1575
+ final response = await request.close();
1576
+ final bytes = await response.fold<List<int>>(
1577
+ <int>[],
1578
+ (all, chunk) => all..addAll(chunk),
1579
+ );
1580
+ if (response.statusCode != HttpStatus.ok || bytes.isEmpty) {
1581
+ throw NomosTestHarnessException(
1582
+ 'cloudBound: could not pin runtime artifact ${artifact.name} before going offline '
1583
+ '(status ${response.statusCode}, ${bytes.length} bytes)',
1584
+ );
1585
+ }
1586
+ await File('${localRuntime.path}/${artifact.fileName}')
1587
+ .writeAsBytes(bytes, flush: true);
1545
1588
  }
1546
- await File(localWasm).writeAsBytes(bytes, flush: true);
1547
1589
  },
1548
1590
  );
1549
1591
  }
@@ -1551,7 +1593,7 @@ class NomosTestHarness {
1551
1593
  ...Platform.environment,
1552
1594
  if (cloud.testKRootPublic != null)
1553
1595
  'NOMOS_TEST_K_ROOT_PUBLIC': cloud.testKRootPublic!,
1554
- 'NOMOS_LOCAL_WASM': localWasm,
1596
+ 'NOMOS_LOCAL_RUNTIME_DIR': localRuntime.path,
1555
1597
  };
1556
1598
  transport = await _phased(
1557
1599
  phase: '2/6: spawn the node runner (cloud-bound)',
@@ -1684,8 +1726,7 @@ class NomosTestHarness {
1684
1726
  // 5/6 — THE HOME CEREMONY. Author the birth in the already-mounted PARENT kernel, then sync its sealed
1685
1727
  // intent to the remote parent where the same offer gate replays it. The HTTP host never receives
1686
1728
  // business fields or a signing secret and has no birth-authoring lane. The offer carries the
1687
- // device-signed homeCert + the owner's device keyHash (the home's genesis ENROLS it, so the born
1688
- // home is SIGNER-READY — it can birth its own estates/catalogues from intent 0).
1729
+ // typed device-signed certificate. The kernel derives and enrolls the founding signer from it.
1689
1730
  String? home;
1690
1731
  await _phased(
1691
1732
  phase: '5/6: remote home-birth ceremony (attested)',
@@ -1700,31 +1741,33 @@ class NomosTestHarness {
1700
1741
  // platform session explicitly; relying on the single-session convenience API becomes
1701
1742
  // ambiguous as soon as the ceremony correctly opens the target workspace in-kernel.
1702
1743
  final homeCert =
1703
- await plane.session('$clientId-platform-author').signBirthCert({
1704
- 'child': homeName,
1705
- 'founder': subject,
1706
- // The cert's `verified` must MATCH the gate verdict (CAS-as-law): the offer carries the
1707
- // dev-IdP token, so the gate resolves a VERIFIED founder.
1708
- 'verified': true,
1709
- 'parentKey': device.publicB64,
1710
- 'parentId': platformWorkspace,
1711
- 'epoch': 0,
1712
- }, device.secret);
1744
+ await plane.session('$clientId-platform-author').signBirthCert(
1745
+ NomosBirthCertificateSpec(
1746
+ child: homeName,
1747
+ founder: subject,
1748
+ // The cert's `verified` must MATCH the gate verdict (CAS-as-law): the offer carries
1749
+ // the dev-IdP token, so the gate resolves a VERIFIED founder.
1750
+ verified: true,
1751
+ parentKey: device.publicB64,
1752
+ parentId: platformWorkspace,
1753
+ epoch: 0,
1754
+ ),
1755
+ device.secret,
1756
+ );
1713
1757
  final localBirth =
1714
1758
  await plane.session('$clientId-platform-author').offerOutcome(
1715
- domain: applicationHomeDomain,
1716
- directiveId: 'nomosInternalBirthReceivingGroup7c1f5e',
1717
- actor: subject,
1718
- authorSecret: device.secret,
1719
- payload: <String, Object?>{
1720
- 'owner': subject,
1721
- 'ownerKeyHash': device.keyHash,
1722
- 'frameworkHash': frameworkHash,
1723
- 'lawHash': applicationHomeLawHash,
1724
- 'bornAt': DateTime.now().toUtc().toIso8601String(),
1725
- 'homeCert': jsonEncode(homeCert),
1726
- },
1727
- );
1759
+ domain: applicationHomeDomain,
1760
+ directiveId: 'nomosInternalBirthReceivingGroup7c1f5e',
1761
+ actor: subject,
1762
+ authorSecret: device.secret,
1763
+ payload: <String, Object?>{
1764
+ 'owner': subject,
1765
+ 'frameworkHash': frameworkHash,
1766
+ 'lawHash': applicationHomeLawHash,
1767
+ 'bornAt': DateTime.now().toUtc().toIso8601String(),
1768
+ },
1769
+ birthCertificate: homeCert,
1770
+ );
1728
1771
  if (!localBirth.born.contains(homeName)) {
1729
1772
  throw NomosTestHarnessException(
1730
1773
  'cloudBound: the local parent kernel admitted the receiving-group offer but did not birth '
@@ -1774,12 +1817,22 @@ class NomosTestHarness {
1774
1817
  // A PRIVATE home (declared read caps — co2's shape) serves its ledger only to a VERIFIED
1775
1818
  // principal: ride the caller's token when given, else the dev-IdP token the ceremony minted.
1776
1819
  authToken: authToken ?? device.idpToken,
1777
- // The home was born WARRANTED with THIS device enrolled in its genesis (ownerKeyHash) —
1820
+ // The home was born WARRANTED with THIS device enrolled by the kernel-owned attestation step —
1778
1821
  // connect with the same key so every write is signed and the home's own `.births()` offers
1779
1822
  // (estates/catalogues) are device-attested (the client signs the birth cert automatically).
1780
1823
  authorSecret: device.secret,
1781
1824
  ),
1782
1825
  );
1826
+ // A cloud-bound generated application owns continuous convergence from the moment its home is
1827
+ // mounted. Starting it here makes the harness exercise the product lifecycle: local writes flush,
1828
+ // transport debt survives a partition, and reconnect settles exact operation receipts without tenant
1829
+ // code calling sync(). Child sessions take the same path in acquireSession().
1830
+ final applicationSession = plane.session(home!);
1831
+ applicationSession.nomosInternalRetainCloudAccess(
1832
+ cloud: cloud.origin,
1833
+ signIn: () async => authToken ?? device.idpToken,
1834
+ );
1835
+ await applicationSession.autoSync(fallbackMs: 60000);
1783
1836
  // NO DECOY on a cloudBound harness: the app is genuinely multi-workspace (the home plus every
1784
1837
  // locally-born child), so the plane is never sole-session — the decoy the offline harness
1785
1838
  // needs (to break single-session inference) would just be a spurious local-only workspace here.
@@ -1792,9 +1845,7 @@ class NomosTestHarness {
1792
1845
  workdir,
1793
1846
  onDiagnostic,
1794
1847
  [home!],
1795
- // The owner's device (enrolled in the home's genesis via ownerKeyHash) — the app derives the
1796
- // keyHash for its own `.births()` payloads from [devicePublicKey] and signs locally-born-child
1797
- // sessions with [deviceSecret].
1848
+ // The owner's device (enrolled by the typed birth attestation) signs later child births locally.
1798
1849
  devicePublics: {home!: device.publicB64},
1799
1850
  deviceSecrets: {home!: device.secret},
1800
1851
  frameworkHash: frameworkHash,