@githolon/dsl 0.101.6 → 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.
- package/dart/CHANGELOG.md +13 -0
- package/dart/lib/nomos_client.dart +3 -52
- package/dart/lib/src/dev_cloud.dart +5 -0
- package/dart/lib/src/home.dart +10 -15
- package/dart/lib/src/nms_conformance_runtime.dart +12 -0
- package/dart/lib/src/nms_generated_runtime.dart +29 -0
- package/dart/lib/src/nms_test_environment.dart +8 -0
- package/dart/lib/src/node_runner_transport.dart +56 -18
- package/dart/lib/src/provider.dart +5 -1
- package/dart/lib/src/record_origin.dart +55 -0
- package/dart/lib/src/runner/node-entry.mjs +9105 -0
- package/dart/lib/src/runner/offline-install.mjs +4729 -0
- package/dart/lib/src/session_bound.dart +23 -10
- package/dart/lib/src/source_trust.dart +51 -0
- package/dart/lib/src/test_harness.dart +490 -58
- package/dart/lib/src/webview_bridge.dart +167 -58
- package/dart/lib/src/workspace_name.dart +7 -1
- package/dart/pubspec.lock +3 -3
- package/dart/pubspec.yaml +4 -2
- package/dart/test/acquire_session_credentials_test.dart +34 -17
- package/dart/test/acquire_session_test.dart +109 -24
- package/dart/test/adv_realm_interference_test.dart +227 -99
- package/dart/test/bundle_version_pairing_test.dart +1 -1
- package/dart/test/cloud_bound_topology_test.dart +1 -1
- package/dart/test/cluster_attested_test.dart +52 -35
- package/dart/test/cluster_births_test.dart +5 -2
- package/dart/test/cluster_device_keyed_acceptance_test.dart +5 -2
- package/dart/test/cluster_local_autosync_test.dart +5 -2
- package/dart/test/cluster_scale_test.dart +5 -2
- package/dart/test/compat_test.dart +1 -1
- package/dart/test/dlq_session_surface_test.dart +2 -1
- package/dart/test/errors_test.dart +1 -1
- package/dart/test/first_app_gauntlet_test.dart +78 -51
- package/dart/test/four_layer_nested_topology_test.dart +1 -1
- package/dart/test/harness_phase_timeout_test.dart +1 -1
- package/dart/test/home_parity_test.dart +9 -5
- package/dart/test/local_sync_inert_test.dart +22 -14
- package/dart/test/node_runner_transport_test.dart +9 -5
- package/dart/test/null_slot_test.dart +1 -1
- package/dart/test/parent_signer_recovery_test.dart +5 -2
- package/dart/test/schema_validation_test.dart +1 -1
- package/dart/test/sealing_client_test.dart +2 -1
- package/dart/test/session_bound_test.dart +92 -119
- package/dart/test/stale_bundle_test.dart +1 -1
- package/dart/test/test_harness_test.dart +77 -22
- package/dart/test/vo_read_typing_test.dart +32 -14
- package/dart/test/watch_after_offer_stall_test.dart +2 -1
- package/dart/test/webview_bridge_test.dart +17 -4
- package/dart/test/wire_test.dart +1 -1
- package/dart_types/CHANGELOG.md +8 -0
- package/dart_types/lib/nomos_types.dart +7 -6
- package/dart_types/lib/src/activity.dart +23 -13
- package/dart_types/lib/src/foreign_ref.dart +61 -112
- package/dart_types/lib/src/invitation.dart +96 -0
- package/dart_types/lib/src/nomos_ref.dart +97 -59
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +125 -19
- package/package.json +7 -3
- package/runtime/generated-application-host.js +8851 -0
- package/src/activity.ts +2 -2
- package/src/aggregate.ts +24 -1
- package/src/application_composition.ts +282 -0
- package/src/application_topology.ts +151 -0
- package/src/attested_core.ts +2 -2
- package/src/authoring_context.ts +9 -0
- package/src/birth_meter_ensure.ts +1 -1
- package/src/births.ts +53 -5
- package/src/build_package.ts +79 -71
- package/src/business_birth.ts +93 -10
- package/src/clear_directive.ts +3 -1
- package/src/codegen_dart.ts +369 -420
- package/src/codegen_dart_application.ts +1508 -0
- package/src/codegen_dart_client.ts +156 -95
- package/src/codegen_dart_scenario.ts +299 -0
- package/src/codegen_openapi.ts +8 -3
- package/src/codegen_proof.ts +1113 -1137
- package/src/codegen_provider_dart.ts +12 -1
- package/src/codegen_ts.ts +1176 -496
- package/src/compat_package_main.ts +22 -10
- package/src/compile_package_main.ts +1536 -337
- package/src/contract.ts +10 -4
- package/src/dart_barrel_guard.ts +5 -1
- package/src/describe_model.ts +13 -5
- package/src/directive.ts +244 -5
- package/src/engine_entry.ts +56 -13
- package/src/engine_javascript.ts +31 -0
- package/src/evolve_preview.ts +3 -12
- package/src/framework/application_home.ts +24 -10
- package/src/framework/capability.ts +15 -1
- package/src/framework/domain_lifecycle.ts +8 -0
- package/src/framework/framework_aggregates.ts +2 -2
- package/src/framework/genesis.ts +11 -1
- package/src/framework/home.ts +178 -75
- package/src/framework/impure_capability.ts +24 -7
- package/src/framework/relations.ts +13 -5
- package/src/framework/workspaces.ts +13 -12
- package/src/generated_client_names.ts +42 -3
- package/src/idof_lint.ts +65 -13
- package/src/index.ts +46 -12
- package/src/intent.ts +587 -0
- package/src/law_check.ts +64 -16
- package/src/precondition.ts +16 -4
- package/src/read_projection_namespace.ts +2 -7
- package/src/recipient.ts +33 -0
- package/src/relation_schema.ts +93 -0
- package/src/secondary_ensure.ts +11 -2
- package/src/shape_compat.ts +11 -1
- package/src/usd.ts +30 -8
- package/src/usd_layers.ts +447 -246
- package/src/usd_state.ts +1 -1
- package/src/value_object.ts +49 -13
- package/src/wire_encode.ts +124 -26
- package/src/workspace_sharing.ts +269 -0
- package/src/zod_introspection.ts +84 -0
- package/dart/example/nomos_secure_key_store.dart +0 -82
- package/dart/example/nomos_webview_transport.dart +0 -63
- package/dart/lib/nomos_headless.dart +0 -50
- package/dart/lib/testing.dart +0 -14
- package/dart/test/co2_product_gauntlet_test.dart +0 -244
- 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/
|
|
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 =>
|
|
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())
|
|
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(
|
|
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': {
|
|
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': {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
108
|
+
final byIdRows = await harness.bridge.queryById(siteId);
|
|
94
109
|
expect(byIdRows, hasLength(1));
|
|
95
|
-
final fromById =
|
|
96
|
-
|
|
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
|
|
118
|
+
.watchById(siteId)
|
|
103
119
|
.map((rows) => rows
|
|
104
|
-
.map((r) => Site.fromJson(
|
|
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/
|
|
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/
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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?>>[];
|
package/dart/test/wire_test.dart
CHANGED
package/dart_types/CHANGELOG.md
CHANGED
|
@@ -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
|
-
///
|
|
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
|
-
//
|
|
31
|
-
//
|
|
32
|
-
|
|
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
|
-
|
|
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(
|
|
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 =>
|
|
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
|
-
|
|
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 &&
|
|
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
|
-
///
|
|
77
|
-
final String
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
'
|
|
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(
|
|
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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
///
|
|
78
|
-
///
|
|
79
|
-
class
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
|
|
114
|
-
|
|
77
|
+
@override
|
|
78
|
+
bool operator ==(Object other) =>
|
|
79
|
+
other is ForeignRef<T> && other._ref == _ref;
|
|
115
80
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
NomosRef get ref => NomosRef(workspace: workspace, type: type, id: id);
|
|
81
|
+
@override
|
|
82
|
+
int get hashCode => _ref.hashCode;
|
|
119
83
|
|
|
120
|
-
|
|
121
|
-
|
|
84
|
+
@override
|
|
85
|
+
String toString() => 'ForeignRef<$T>';
|
|
86
|
+
}
|
|
122
87
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
})
|
|
96
|
+
}) =>
|
|
97
|
+
ForeignRef<T>._(
|
|
98
|
+
NmsInternalNomosRefs.create<T>(
|
|
99
|
+
workspace: workspace, type: type, id: id),
|
|
100
|
+
fromJson,
|
|
101
|
+
);
|
|
129
102
|
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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 =
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
193
|
-
|
|
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
|
}
|