@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
@@ -12,7 +12,7 @@ library;
12
12
 
13
13
  import 'dart:io';
14
14
 
15
- import 'package:nomos_client/nomos_headless.dart';
15
+ import 'package:nomos_client/src/nms_conformance_runtime.dart';
16
16
  import 'package:test/test.dart';
17
17
 
18
18
  // The fixture's GENERATED Dart read models (support: "package" → imports package:nomos_client).
@@ -22,10 +22,14 @@ String get _root => '${Directory.current.path}/../..';
22
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
- String get _runtimeDir => '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
25
+ String get _runtimeDir =>
26
+ Platform.environment['NOMOS_DART_RUNTIME'] ??
27
+ Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
28
+ '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
26
29
 
27
30
  String? _missingPrereq() {
28
- if (!File(_repoEntry).existsSync()) return 'in-repo runner not found at $_repoEntry';
31
+ if (!File(_repoEntry).existsSync())
32
+ return 'in-repo runner not found at $_repoEntry';
29
33
  if (!File(_deployJson).existsSync()) {
30
34
  return 'compile the fixture: cd bench/scale/fixtures/vo-read-typing && npx nomos-compile';
31
35
  }
@@ -37,7 +41,9 @@ String? _missingPrereq() {
37
41
  }
38
42
 
39
43
  void main() {
40
- test('OFFLINE: nested VO + map-of-VO read back TYPED via query, by-id, and watch', () async {
44
+ test(
45
+ 'OFFLINE: nested VO + map-of-VO read back TYPED via query, by-id, and watch',
46
+ () async {
41
47
  final skip = _missingPrereq();
42
48
  if (skip != null) {
43
49
  markTestSkipped(skip);
@@ -58,24 +64,33 @@ void main() {
58
64
  'name': 'HQ',
59
65
  'location': {
60
66
  'address': {'streetAddress': '1 High St', 'city': 'Leeds'},
61
- 'point': {'type': 'Point', 'coordinates': [-1.55, 53.8]},
67
+ 'point': {
68
+ 'type': 'Point',
69
+ 'coordinates': [-1.55, 53.8]
70
+ },
62
71
  },
63
72
  'rooms': {
64
73
  'atrium': {
65
74
  'address': {'streetAddress': '1 High St', 'city': 'Leeds'},
66
- 'point': {'type': 'Point', 'coordinates': [-1.5, 53.79]},
75
+ 'point': {
76
+ 'type': 'Point',
77
+ 'coordinates': [-1.5, 53.79]
78
+ },
67
79
  },
68
80
  },
69
81
  },
70
82
  );
71
83
  expect(outcome.created, hasLength(1));
72
- final siteId = SiteId.fromRow(outcome.created.single.id);
84
+ // Deliberately raw framework-mechanism probe: generated tenant applications retain the typed
85
+ // NomosRef<Site>; this test exercises queryById/watchById beneath that sealed surface.
86
+ final siteId = outcome.created.single.id;
73
87
 
74
88
  // ── (1) declared query → typed Site, typed nested VO + typed map-of-VO ──
75
89
  final qRows = await harness.bridge.query('sitesByName', {'name': 'HQ'});
76
90
  expect(qRows, hasLength(1));
77
- final fromQuery =
78
- Site.fromJson((qRows.first['data'] as Map).cast<String, dynamic>(), (qRows.first['id'] ?? '').toString());
91
+ final fromQuery = Site.fromJson(
92
+ (qRows.first['data'] as Map).cast<String, dynamic>(),
93
+ (qRows.first['id'] ?? '').toString());
79
94
 
80
95
  expect(fromQuery.name, 'HQ');
81
96
  // Typed nested VO access — no map indexing, no casts.
@@ -90,18 +105,21 @@ void main() {
90
105
  expect(fromQuery.rooms['atrium']!.address!.city, 'Leeds');
91
106
 
92
107
  // ── (2) by-id read → same typed shape ──
93
- final byIdRows = await harness.bridge.queryById(siteId.value);
108
+ final byIdRows = await harness.bridge.queryById(siteId);
94
109
  expect(byIdRows, hasLength(1));
95
- final fromById =
96
- Site.fromJson((byIdRows.first['data'] as Map).cast<String, dynamic>(), (byIdRows.first['id'] ?? '').toString());
110
+ final fromById = Site.fromJson(
111
+ (byIdRows.first['data'] as Map).cast<String, dynamic>(),
112
+ (byIdRows.first['id'] ?? '').toString());
97
113
  expect(fromById.location!.point!.coordinates, [-1.55, 53.8]);
98
114
  expect(fromById.rooms['atrium']!.point!.coordinates, [-1.5, 53.79]);
99
115
 
100
116
  // ── (3) watch stream → typed rows, identical decode ──
101
117
  final watched = await harness.bridge
102
- .watchById(siteId.value)
118
+ .watchById(siteId)
103
119
  .map((rows) => rows
104
- .map((r) => Site.fromJson((r['data'] as Map).cast<String, dynamic>(), (r['id'] ?? '').toString()))
120
+ .map((r) => Site.fromJson(
121
+ (r['data'] as Map).cast<String, dynamic>(),
122
+ (r['id'] ?? '').toString()))
105
123
  .toList())
106
124
  .firstWhere((rows) => rows.isNotEmpty);
107
125
  expect(watched, hasLength(1));
@@ -8,7 +8,7 @@ library;
8
8
  import 'dart:async';
9
9
  import 'dart:io';
10
10
 
11
- import 'package:nomos_client/nomos_headless.dart';
11
+ import 'package:nomos_client/src/nms_conformance_runtime.dart';
12
12
  import 'package:test/test.dart';
13
13
 
14
14
  String get _root => '${Directory.current.path}/../..';
@@ -16,6 +16,7 @@ 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'] ??
19
+ Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
19
20
  '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
20
21
 
21
22
  String? _missingPrereq() {
@@ -7,7 +7,7 @@ import 'dart:async';
7
7
  import 'dart:convert';
8
8
  import 'dart:io';
9
9
 
10
- import 'package:nomos_client/nomos_client.dart';
10
+ import 'package:nomos_client/src/nms_generated_runtime.dart';
11
11
  import 'package:test/test.dart';
12
12
 
13
13
  /// A fake runner-over-a-transport: records sent requests, replies the way `webview-runner.mjs` does, and can
@@ -16,6 +16,7 @@ class FakeRunner implements BridgeTransport {
16
16
  final _incoming = StreamController<Map<String, Object?>>.broadcast();
17
17
  final List<Map<String, Object?>> sent = [];
18
18
  String? lastWatchSubId;
19
+ bool acknowledgeUnwatch = true;
19
20
 
20
21
  FakeRunner() {
21
22
  // the runner announces readiness at construction.
@@ -234,7 +235,9 @@ class FakeRunner implements BridgeTransport {
234
235
  _incoming.add(reply({'subId': args['subId']}));
235
236
  break;
236
237
  case 'unwatch':
237
- _incoming.add(reply({'subId': args['subId']}));
238
+ if (acknowledgeUnwatch) {
239
+ _incoming.add(reply({'subId': args['subId']}));
240
+ }
238
241
  break;
239
242
  default:
240
243
  _incoming.add({
@@ -631,7 +634,7 @@ void main() {
631
634
  final moments = await momentsBridge.moments(limit: 12);
632
635
  expect(moments, hasLength(1));
633
636
  expect(moments.single.id, 'abc123');
634
- expect(moments.single.actorSubject, 'user:ada');
637
+ expect(moments.single.performedBy, 'user:ada');
635
638
  expect(moments.single.action, 'createSite');
636
639
  expect((momentsTransport.lastRequest!['args'] as Map)['limit'], 12);
637
640
  await momentsBridge.dispose();
@@ -720,7 +723,7 @@ void main() {
720
723
  final payload = await bridge.nomosPrepareOwnedRecordAction(
721
724
  action: 'share',
722
725
  scope: 'owned-reader',
723
- record: const NomosRef(
726
+ record: NmsInternalNomosRefs.create<Object?>(
724
727
  workspace: 'ws-1',
725
728
  type: 'notes.Note',
726
729
  id: 'note-1',
@@ -805,6 +808,16 @@ void main() {
805
808
  expect(runner.sent.any((m) => m['op'] == 'unwatch'), isTrue);
806
809
  });
807
810
 
811
+ test('watch cancellation is local and never waits for the runner', () async {
812
+ await bridge.ready;
813
+ runner.acknowledgeUnwatch = false;
814
+ final sub = bridge.watch('byAuthor', {'author': 'ada'}).listen((_) {});
815
+ await Future<void>.delayed(Duration.zero);
816
+
817
+ await sub.cancel().timeout(const Duration(milliseconds: 100));
818
+ expect(runner.sent.any((message) => message['op'] == 'unwatch'), isTrue);
819
+ });
820
+
808
821
  test('signal events are forwarded on their own stream', () async {
809
822
  await bridge.ready;
810
823
  final got = <Map<String, Object?>>[];
@@ -7,7 +7,7 @@
7
7
 
8
8
  import 'dart:convert';
9
9
 
10
- import 'package:nomos_client/nomos_client.dart';
10
+ import 'package:nomos_client/src/nms_generated_runtime.dart';
11
11
  import 'package:test/test.dart';
12
12
 
13
13
  /// Deep round-trip: a JSON string -> Dart fromJson -> toJson -> re-encoded
@@ -1,3 +1,11 @@
1
+ ## 0.8.0
2
+
3
+ - Make `NomosRef<T>` the canonical identity of generated business records. References carry their workspace,
4
+ aggregate type and record id through actions, queries, navigation and activity without falling back to
5
+ interchangeable strings.
6
+ - Keep generated business values and framework lifecycle details on separate package boundaries so product
7
+ code cannot accidentally rebuild Nomos runtime state.
8
+
1
9
  ## 0.7.1
2
10
 
3
11
  - Add typed framework recovery actions and lifecycle failure shapes without leaking signer, certificate,
@@ -7,7 +7,7 @@
7
7
  /// NO bridge, NO runtime, NO Flutter. Generated per-domain TYPES files depend on ONLY
8
8
  /// this, so they resolve without pulling in sessions/workspace lifecycle. The
9
9
  /// `nomos_client` package depends on and re-exports this barrel, so existing
10
- /// `package:nomos_client/nomos_client.dart` imports are unchanged.
10
+ /// Generated application packages consume this pure data layer through their implementation code.
11
11
  ///
12
12
  /// - The kernel WIRE types (`Value`, `Field`/`WireField`, `Hlc`, `Op`, `FieldOp`,
13
13
  /// `Event`, `Schema`/`WireDriver`) — byte-compatible with serde_json.
@@ -27,12 +27,13 @@ export 'src/schema_validation.dart';
27
27
  export 'src/aggregate_meta.dart';
28
28
  export 'src/error_info.dart';
29
29
  export 'src/errors.dart';
30
- // The FOREIGN-REF locator runtime (t.foreignRef): the typed cross-workspace `ForeignRef<T>` + the structural
31
- // `NomosForeignReader`/`NomosForeignWorkspaceReader` read surfaces + the named resolve outcomes. Pure data +
32
- // abstract interfaces (no bridge) — so both the layered TYPES package and nomos_client resolve them.
33
- export 'src/foreign_ref.dart';
30
+ // Foreign references expose identity data, while navigation readers and attestation stay behind the
31
+ // generated application runtime.
32
+ export 'src/foreign_ref.dart' show ForeignRef;
34
33
  export 'src/activity.dart';
35
- export 'src/nomos_ref.dart';
34
+ // NomosRef is inspectable but framework-created. Its constructor and wire codec stay behind the runtime.
35
+ export 'src/nomos_ref.dart' show NomosRef;
36
+ export 'src/invitation.dart' show Identity, User, Recipient, Invitation;
36
37
  // Branded identity wrappers (NomosPrincipal / NomosDeviceSecret) for the generated cross-workspace
37
38
  // birth methods — actor/authorSecret are distinct types, so a transposition is a compile error.
38
39
  export 'src/principal.dart';
@@ -20,21 +20,29 @@ class ActivityRef {
20
20
  /// The source workspace (additive — empty on rows rendered before refs carried it).
21
21
  final String workspace;
22
22
 
23
- const ActivityRef({required this.type, required this.id, this.workspace = ''});
23
+ const ActivityRef(
24
+ {required this.type, required this.id, this.workspace = ''});
24
25
 
25
26
  /// The canonical reference view (`nomos://<workspace>/<type>/<id>`).
26
- NomosRef get ref => NomosRef(workspace: workspace, type: type, id: id);
27
+ NomosRef<Object?> get ref => NmsInternalNomosRefs.create<Object?>(
28
+ workspace: workspace, type: type, id: id);
27
29
 
28
30
  factory ActivityRef.fromJson(Map<String, dynamic> j) => ActivityRef(
29
31
  type: (j['type'] ?? '') as String,
30
32
  id: (j['id'] ?? '') as String,
31
33
  workspace: (j['workspace'] ?? '') as String);
32
- Map<String, dynamic> toJson() =>
33
- {'type': type, 'id': id, if (workspace.isNotEmpty) 'workspace': workspace};
34
+ Map<String, dynamic> toJson() => {
35
+ 'type': type,
36
+ 'id': id,
37
+ if (workspace.isNotEmpty) 'workspace': workspace
38
+ };
34
39
 
35
40
  @override
36
41
  bool operator ==(Object other) =>
37
- other is ActivityRef && other.type == type && other.id == id && other.workspace == workspace;
42
+ other is ActivityRef &&
43
+ other.type == type &&
44
+ other.id == id &&
45
+ other.workspace == workspace;
38
46
  @override
39
47
  int get hashCode => Object.hash(type, id, workspace);
40
48
  }
@@ -73,8 +81,8 @@ class ActivityEvent {
73
81
  /// The declared STABLE event kind (e.g. `"site.created"`).
74
82
  final String kind;
75
83
 
76
- /// The intent's author (principal) — the "who".
77
- final String actorSubject;
84
+ /// Who performed the business action.
85
+ final String performedBy;
78
86
 
79
87
  /// When it happened (the intent's physical HLC timestamp, ISO-8601 when available).
80
88
  final String occurredAt;
@@ -113,7 +121,7 @@ class ActivityEvent {
113
121
  const ActivityEvent({
114
122
  required this.eventId,
115
123
  required this.kind,
116
- required this.actorSubject,
124
+ required this.performedBy,
117
125
  required this.occurredAt,
118
126
  required this.title,
119
127
  required this.message,
@@ -131,7 +139,7 @@ class ActivityEvent {
131
139
  factory ActivityEvent.fromJson(Map<String, dynamic> j) => ActivityEvent(
132
140
  eventId: (j['eventId'] ?? '') as String,
133
141
  kind: (j['kind'] ?? '') as String,
134
- actorSubject: (j['actorSubject'] ?? '') as String,
142
+ performedBy: (j['performedBy'] ?? j['actorSubject'] ?? '') as String,
135
143
  occurredAt: (j['occurredAt'] ?? '') as String,
136
144
  title: (j['title'] ?? '') as String,
137
145
  message: (j['message'] ?? '') as String,
@@ -139,8 +147,9 @@ class ActivityEvent {
139
147
  messageKey: (j['messageKey'] ?? '') as String,
140
148
  params: ((j['params'] as Map?)?.cast<String, dynamic>() ?? const {})
141
149
  .map((k, v) => MapEntry(k, '$v')),
142
- refs: ((j['refs'] as Map?)?.cast<String, dynamic>() ?? const {})
143
- .map((k, v) => MapEntry(k, ActivityRef.fromJson((v as Map).cast<String, dynamic>()))),
150
+ refs: ((j['refs'] as Map?)?.cast<String, dynamic>() ?? const {}).map(
151
+ (k, v) => MapEntry(
152
+ k, ActivityRef.fromJson((v as Map).cast<String, dynamic>()))),
144
153
  category: j['category'] as String?,
145
154
  icon: j['icon'] as String?,
146
155
  severity: ActivitySeverity.fromWire(j['severity'] as String?),
@@ -151,7 +160,7 @@ class ActivityEvent {
151
160
  Map<String, dynamic> toJson() => {
152
161
  'eventId': eventId,
153
162
  'kind': kind,
154
- 'actorSubject': actorSubject,
163
+ 'performedBy': performedBy,
155
164
  'occurredAt': occurredAt,
156
165
  'title': title,
157
166
  'message': message,
@@ -170,4 +179,5 @@ class ActivityEvent {
170
179
  /// Interpolate `{name}` placeholders in [template] from [vars] — the render helper the generated
171
180
  /// `renderActivity` uses. A missing var renders empty (the compiler already validated every placeholder).
172
181
  String renderActivityTemplate(String template, Map<String, String> vars) =>
173
- template.replaceAllMapped(RegExp(r'\{([a-zA-Z0-9_]+)\}'), (m) => vars[m.group(1)] ?? '');
182
+ template.replaceAllMapped(
183
+ RegExp(r'\{([a-zA-Z0-9_]+)\}'), (m) => vars[m.group(1)] ?? '');
@@ -1,13 +1,6 @@
1
- // NOMOS — the FOREIGN-REF runtime (t.foreignRef): a TYPED CROSS-WORKSPACE LOCATOR, the Dart twin of the
2
- // TS client's `ForeignRef`/`NomosForeignReader` (codegen_ts.ts). A `t.foreignRef({ workspace, target })`
3
- // field stores the foreign aggregate's id, alongside a SIBLING field on the same aggregate holding the
4
- // foreign WORKSPACE id (so the target workspace is dynamic per row). The generated read model surfaces the
5
- // field as a `ForeignRef<T>` — NOT a raw string — so the app can `resolve`/`watch`/`attest` the target on
6
- // demand WITHOUT cloning the other holon: it opens the foreign workspace's read surface lazily.
7
- //
8
- // A ForeignRef is a LOCATOR, not an atomic foreign key: reading it is informational (resolve/exists/watch);
9
- // only `attest` is law-grade (signed evidence the reference resolves) and it works ONLY when the law
10
- // DECLARES the dependency (`trustSource` + `.readsFrom`) — otherwise it throws, by design.
1
+ // NOMOS — the FOREIGN-REF runtime (t.foreignRef): a typed cross-application business reference.
2
+ // Tenant code may retain and pass the reference to generated business methods. The generated application
3
+ // implementation alone can decode or follow it; workspace routing and evidence remain framework concerns.
11
4
  //
12
5
  // These types live in the pure `nomos_types` core (no bridge, no lifecycle) so BOTH the layered TYPES
13
6
  // package (frontend-safe) AND the runtime `nomos_client` package resolve them; `nomos_client` re-exports
@@ -23,15 +16,9 @@ import 'nomos_ref.dart';
23
16
  abstract class NomosForeignReader {
24
17
  /// A read handle for [workspace] (mounted on the local plane on demand, never cloned).
25
18
  NomosForeignWorkspaceReader foreignWorkspace(String workspace);
26
-
27
- /// The LAW-GRADE attestation hook — non-`null` ONLY when the app wires the attested-read lane
28
- /// (`trustSource` + `.readsFrom`). `null` here ⇒ [ForeignRef.attest] throws, by design (a locator is not an
29
- /// atomic foreign key). Override in a reader that carries the attested-read lane.
30
- Future<ForeignAttestation> Function(String workspace, String id, String type)?
31
- get attestForeign => null;
32
19
  }
33
20
 
34
- /// One foreign workspace's by-id read surface (used by [ForeignRef.resolve]/[ForeignRef.watch]). Returns the
21
+ /// One foreign workspace's by-id read surface. Returns the
35
22
  /// RAW projection rows (`[{type,id,data}, …]`) — the ForeignRef decodes the matching row's flat `data` map
36
23
  /// via the target `fromJson` it captured at construction (the same shape [NomosReads.decodeRows] consumes).
37
24
  abstract class NomosForeignWorkspaceReader {
@@ -40,13 +27,13 @@ abstract class NomosForeignWorkspaceReader {
40
27
  Future<List<Map<String, dynamic>>> queryById(String id);
41
28
 
42
29
  /// OPTIONAL reactive by-id read — the target's rows re-emitted on each projection tick. `null` when this
43
- /// reader offers no watch lane (then [ForeignRef.watch] calls back once with `null` and no-ops).
30
+ /// reader offers no watch lane (then the internal watcher calls back once with `null` and no-ops).
44
31
  Stream<List<Map<String, dynamic>>>? watchById(String id) => null;
45
32
  }
46
33
 
47
- /// The OUTCOME of a [ForeignRef.resolveResult], with the reason NAMED — a foreign read can come back empty
34
+ /// The outcome of an internal foreign-reference resolution, with the reason named — a foreign read can come back empty
48
35
  /// because the target does not exist, the reader lacks access, or the holon is unreachable, and those are
49
- /// very different. [ForeignRef.resolve] collapses them all to `null`; use this when the WHY matters.
36
+ /// very different. The generated application collapses these details into its business-shaped API.
50
37
  sealed class ForeignResolveResult<T> {
51
38
  const ForeignResolveResult();
52
39
  }
@@ -74,64 +61,49 @@ class ForeignUnreachable<T> extends ForeignResolveResult<T> {
74
61
  const ForeignUnreachable(this.error);
75
62
  }
76
63
 
77
- /// LAW-GRADE signed evidence that a [ForeignRef] resolves — returned by [ForeignRef.attest] on the
78
- /// attested-read lane (`trustSource` + `.readsFrom`). Pure data.
79
- class ForeignAttestation {
80
- /// The foreign aggregate's id the attestation is over.
81
- final String id;
82
-
83
- /// The foreign workspace the attestation is over.
84
- final String workspace;
85
-
86
- /// Whether the target existed at attestation time.
87
- final bool exists;
88
-
89
- /// The opaque signed attestation blob (the law-grade evidence).
90
- final String attestation;
91
-
92
- const ForeignAttestation({
93
- required this.id,
94
- required this.workspace,
95
- required this.exists,
96
- required this.attestation,
97
- });
98
- }
64
+ /// A typed, inspectable cross-application business reference. Tenant code can retain, serialize and pass
65
+ /// it back to generated application methods; navigation, decoding and attestation remain framework state.
66
+ final class ForeignRef<T> {
67
+ final NomosRef<T> _ref;
68
+ final T Function(Map<String, dynamic> data, [String id]) _decode;
99
69
 
100
- /// A TYPED CROSS-WORKSPACE LOCATOR (`t.foreignRef`) — the foreign aggregate's [id] + the foreign [workspace]
101
- /// (from the declared sibling workspace field) + the target aggregate [type], plus the target `fromJson`
102
- /// captured at construction so [resolve]/[watch] can DECODE it. NOT an atomic foreign key: [resolve]/[watch]
103
- /// open the foreign holon ON DEMAND (no clone). [exists] is the cheap "is it still there?" check; [attest] is
104
- /// the law-grade one (see [ForeignAttestation]). The generated read model builds these from a projection row
105
- /// (the raw id field + the sibling workspace field); [id] stays the raw stored id.
106
- class ForeignRef<T> {
107
- /// The foreign aggregate's id (this field's stored value).
108
- final String id;
70
+ const ForeignRef._(this._ref, this._decode);
109
71
 
110
- /// The foreign workspace id (from the declared sibling workspace field).
111
- final String workspace;
72
+ String get workspace => _ref.workspace;
73
+ String get type => _ref.type;
74
+ String get id => _ref.id;
75
+ NomosRef<T> get ref => _ref;
112
76
 
113
- /// The foreign aggregate type this locator points at.
114
- final String type;
77
+ @override
78
+ bool operator ==(Object other) =>
79
+ other is ForeignRef<T> && other._ref == _ref;
115
80
 
116
- /// The canonical reference view (`nomos://<workspace>/<type>/<id>`) — a [ForeignRef] IS a typed view over
117
- /// the one reference triple.
118
- NomosRef get ref => NomosRef(workspace: workspace, type: type, id: id);
81
+ @override
82
+ int get hashCode => _ref.hashCode;
119
83
 
120
- /// The target's decoder (the aggregate `fromJson` factory), applied to a resolved row's flat `data` map.
121
- final T Function(Map<String, dynamic> data, [String id]) _decode;
84
+ @override
85
+ String toString() => 'ForeignRef<$T>';
86
+ }
122
87
 
123
- const ForeignRef({
124
- required this.id,
125
- required this.workspace,
126
- required this.type,
88
+ /// The one private codec/navigation door used by generated application implementation code.
89
+ /// It is intentionally excluded from `package:nomos_types/nomos_types.dart`.
90
+ abstract final class NmsInternalForeignRefs {
91
+ static ForeignRef<T> create<T>({
92
+ required String id,
93
+ required String workspace,
94
+ required String type,
127
95
  required T Function(Map<String, dynamic> data, [String id]) fromJson,
128
- }) : _decode = fromJson;
96
+ }) =>
97
+ ForeignRef<T>._(
98
+ NmsInternalNomosRefs.create<T>(
99
+ workspace: workspace, type: type, id: id),
100
+ fromJson,
101
+ );
129
102
 
130
- /// Pick the matching row (by [id], else the first) from a raw `{type,id,data}` row list and decode its flat
131
- /// `data` via the captured `fromJson` (threading the row `id`, mirroring [NomosReads.decodeRows]). `null`
132
- /// when the list is empty or the matched row carries no object `data`.
133
- T? _decodeRows(List<Map<String, dynamic>> rows) {
103
+ static T? _decodeRows<T>(
104
+ ForeignRef<T> reference, List<Map<String, dynamic>> rows) {
134
105
  if (rows.isEmpty) return null;
106
+ final id = NmsInternalNomosRefs.id(reference._ref);
135
107
  Map<String, dynamic>? match;
136
108
  for (final r in rows) {
137
109
  if (r['id'] == id) {
@@ -144,35 +116,37 @@ class ForeignRef<T> {
144
116
  if (data is! Map<String, dynamic>) return null;
145
117
  final rawId = match['id'];
146
118
  final rowId = rawId is String ? rawId : (rawId?.toString() ?? id);
147
- return _decode(data, rowId);
119
+ return reference._decode(data, rowId);
148
120
  }
149
121
 
150
- /// THE LOOKUP WITH THE OUTCOME NAMED — found / not-found / denied / unreachable. Opens the foreign
151
- /// workspace's read surface, reads the target by [id], and classifies an empty/errored read (a thrown
152
- /// auth/permission-looking error ⇒ denied; any other throw ⇒ unreachable; an empty read ⇒ not-found).
153
- Future<ForeignResolveResult<T>> resolveResult(NomosForeignReader nomos) async {
122
+ static Future<ForeignResolveResult<T>> resolveResult<T>(
123
+ ForeignRef<T> reference, NomosForeignReader nomos) async {
124
+ final id = NmsInternalNomosRefs.id(reference._ref);
125
+ final workspace = NmsInternalNomosRefs.workspace(reference._ref);
154
126
  if (id.isEmpty || workspace.isEmpty) return ForeignNotFound<T>();
155
127
  try {
156
128
  final rows = await nomos.foreignWorkspace(workspace).queryById(id);
157
- final row = _decodeRows(rows);
129
+ final row = _decodeRows(reference, rows);
158
130
  return row != null ? ForeignFound<T>(row) : ForeignNotFound<T>();
159
131
  } catch (error) {
160
132
  final m = error.toString().toLowerCase();
161
- final denied = RegExp(r'forbid|denied|permission|unauthor|not permitted|403|401').hasMatch(m);
133
+ final denied =
134
+ RegExp(r'forbid|denied|permission|unauthor|not permitted|403|401')
135
+ .hasMatch(m);
162
136
  return denied ? ForeignDenied<T>(error) : ForeignUnreachable<T>(error);
163
137
  }
164
138
  }
165
139
 
166
- /// THE LOOKUP (the 99% case): open the foreign workspace + read the target by [id] — on demand, no clone.
167
- /// Returns `null` for ANY empty outcome (not-found / denied / unreachable); use [resolveResult] for the WHY.
168
- Future<T?> resolve(NomosForeignReader nomos) async {
169
- final r = await resolveResult(nomos);
140
+ static Future<T?> resolve<T>(
141
+ ForeignRef<T> reference, NomosForeignReader nomos) async {
142
+ final r = await resolveResult(reference, nomos);
170
143
  return r is ForeignFound<T> ? r.row : null;
171
144
  }
172
145
 
173
- /// Subscribe to the target's read model across the workspace boundary. Returns an unsubscribe fn. Calls
174
- /// [cb] with `null` once (and no-ops) when the id/workspace is empty or the reader offers no watch lane.
175
- void Function() watch(NomosForeignReader nomos, void Function(T? row) cb) {
146
+ static void Function() watch<T>(ForeignRef<T> reference,
147
+ NomosForeignReader nomos, void Function(T? row) cb) {
148
+ final id = NmsInternalNomosRefs.id(reference._ref);
149
+ final workspace = NmsInternalNomosRefs.workspace(reference._ref);
176
150
  if (id.isEmpty || workspace.isEmpty) {
177
151
  cb(null);
178
152
  return () {};
@@ -183,38 +157,13 @@ class ForeignRef<T> {
183
157
  return () {};
184
158
  }
185
159
  final sub = stream.listen(
186
- (rows) => cb(_decodeRows(rows)),
160
+ (rows) => cb(_decodeRows(reference, rows)),
187
161
  onError: (Object _) => cb(null),
188
162
  );
189
163
  return sub.cancel;
190
164
  }
191
165
 
192
- /// Cheap existence check — does the referenced target still exist? (found ⇒ `true`; anything else ⇒ `false`).
193
- Future<bool> exists(NomosForeignReader nomos) async =>
194
- (await resolveResult(nomos)) is ForeignFound<T>;
195
-
196
- /// LAW-GRADE: signed evidence that the reference resolves, for when your law DEPENDS on the foreign fact.
197
- /// Requires the attested-read lane — declare `trustSource(workspace, …)` + `.readsFrom(…)` on the directive;
198
- /// without it (no `attestForeign` on the reader) this throws, by design (a locator is not an atomic FK).
199
- Future<ForeignAttestation> attest(NomosForeignReader nomos) {
200
- final hook = nomos.attestForeign;
201
- if (hook == null) {
202
- throw StateError(
203
- 'attest() needs the attested-read lane — declare trustSource("$workspace", …) + '
204
- '.readsFrom(…) on the directive whose law depends on this reference '
205
- '(a foreignRef is a locator, not an atomic foreign key)',
206
- );
207
- }
208
- return hook(workspace, id, type);
209
- }
210
-
211
- @override
212
- bool operator ==(Object other) =>
213
- other is ForeignRef<T> &&
214
- other.id == id &&
215
- other.workspace == workspace &&
216
- other.type == type;
217
-
218
- @override
219
- int get hashCode => Object.hash(id, workspace, type);
166
+ static Future<bool> exists<T>(
167
+ ForeignRef<T> reference, NomosForeignReader nomos) async =>
168
+ (await resolveResult(reference, nomos)) is ForeignFound<T>;
220
169
  }