@githolon/dsl 0.67.0 → 0.69.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 CHANGED
@@ -1,3 +1,17 @@
1
+ ## 0.46.0
2
+
3
+ - Custody lifecycle on `NomosBridge`/`NomosSession`: `connect(..., onCustodyBreak: CustodyBreakPolicy)`,
4
+ `custodyStatus()`, the `custodyEvents` stream, `resolveCustodyBreak()` (the ask lane — put the decision in
5
+ front of the user), `salvagedIntents`/`reofferSalvaged`/`discardSalvaged`. A rebirth/supersession is a
6
+ first-class typed event: named truthfully, decided by the app, local work never silently adopted nor lost.
7
+ Requires `nomos_types >= 0.1.4`.
8
+
9
+ ## 0.45.0
10
+
11
+ - `offerOutcome(...)` — the one outcome-returning offer lane generated clients now call for EVERY directive:
12
+ returns `NomosOfferOutcome` with workspace-stamped `created` (`NomosCreated` gains `workspace` + a
13
+ `NomosRef get ref` view). Activity raw rows carry `created` + `workspace`. Requires `nomos_types >= 0.1.3`.
14
+
1
15
  ## 0.44.0
2
16
 
3
17
  - Activity-feed runtime reads on `NomosBridge`/`NomosSession`: `activityLog({base, limit})` — the committed
@@ -23,9 +23,13 @@ import 'package:nomos_types/nomos_types.dart'
23
23
  nomosErrorFromInfo,
24
24
  NomosTraceContext,
25
25
  NomosErrorInfo,
26
+ NomosRef,
26
27
  NomosForeignReader,
27
28
  NomosForeignWorkspaceReader,
28
- ForeignAttestation;
29
+ ForeignAttestation,
30
+ CustodyBreakPolicy,
31
+ NomosCustodyEvent,
32
+ NomosCustodyStatus;
29
33
 
30
34
  /// The protocol version the runner and the bridge agree on (`webview-runner.mjs` `WEBVIEW_BRIDGE_VERSION`).
31
35
  const int kWebviewBridgeVersion = 1;
@@ -104,7 +108,15 @@ class SyncStatus {
104
108
  class NomosCreated {
105
109
  final String type;
106
110
  final String id;
107
- const NomosCreated({required this.type, required this.id});
111
+
112
+ /// The workspace the offer ran on (NomosRef). Additive — defaults to '' on older runners that
113
+ /// don't stamp it; then [ref] is workspace-blank (still a valid locator on the local plane).
114
+ final String workspace;
115
+ const NomosCreated({required this.type, required this.id, this.workspace = ''});
116
+
117
+ /// The canonical [NomosRef] view — the `{workspace,type,id}` triple, followable/serializable
118
+ /// (`nomos://<workspace>/<type>/<id>`). One reference, everywhere.
119
+ NomosRef get ref => NomosRef(workspace: workspace, type: type, id: id);
108
120
  @override
109
121
  String toString() => id;
110
122
  }
@@ -604,6 +616,10 @@ class NomosOfferOutcome {
604
616
  final String head;
605
617
  final List<NomosCreated> created;
606
618
 
619
+ /// The workspace the offer ran on (NomosRef). Additive; '' on older runners. Each [created] ref already
620
+ /// carries this, so `outcome.created.first.ref` is the full followable `{workspace,type,id}` locator.
621
+ final String workspace;
622
+
607
623
  /// The born child workspace name(s), when the offer ran a `.births()` directive (else empty). Each is a
608
624
  /// LOCAL workspace on this device's plane, resident in the shared engine plane the moment it is born:
609
625
  /// `bridge.session(name)` returns its handle, and `bridge.connect(session: name)` (no `restoreFrom`) mounts
@@ -616,6 +632,7 @@ class NomosOfferOutcome {
616
632
  const NomosOfferOutcome({
617
633
  required this.head,
618
634
  required this.created,
635
+ this.workspace = '',
619
636
  this.born = const [],
620
637
  this.birthOutcomes = const [],
621
638
  });
@@ -642,6 +659,11 @@ class NomosBridge {
642
659
  StreamController<Map<String, Object?>>.broadcast();
643
660
  final StreamController<Map<String, Object?>> _signalEvents =
644
661
  StreamController<Map<String, Object?>>.broadcast();
662
+ // CUSTODY LIFECYCLE (rebirth_and_custody_lifecycle.md): each `custody` event carries the raw map (session +
663
+ // custody:{kind,break,…}); a Flutter app binds a break dialog / posture banner to this. Kind "ask" awaits
664
+ // [resolveCustodyBreak].
665
+ final StreamController<NomosCustodyEvent> _custodyEvents =
666
+ StreamController<NomosCustodyEvent>.broadcast();
645
667
  final StreamController<Map<String, Object?>> _flameEvents =
646
668
  StreamController<Map<String, Object?>>.broadcast();
647
669
  final StreamController<Map<String, Object?>> _spanEvents =
@@ -757,6 +779,13 @@ class NomosBridge {
757
779
  }
758
780
  return;
759
781
  }
782
+ if (event == 'custody') {
783
+ // Custody lifecycle: { event:'custody', session, custody:{ kind, break, … } }. Decode + fan out so the
784
+ // app can react (kind "ask" → show a dialog then call resolveCustodyBreak; others are informational).
785
+ final ev = NomosCustodyEvent.fromMessage(m);
786
+ if (ev != null && !_custodyEvents.isClosed) _custodyEvents.add(ev);
787
+ return;
788
+ }
760
789
  if (event == 'transfer') {
761
790
  // LIVE pack byte-progress { received, total, kind } — the real loading bar (host Content-Length).
762
791
  final tp = TransferProgress.fromMessage(m);
@@ -878,6 +907,7 @@ class NomosBridge {
878
907
  String? restoreFrom,
879
908
  bool allowUnborn = false,
880
909
  bool recover = false,
910
+ CustodyBreakPolicy? onCustodyBreak,
881
911
  String? session,
882
912
  int? maxMounted}) async {
883
913
  final ws = session ?? workspace;
@@ -903,10 +933,51 @@ class NomosBridge {
903
933
  // RECOVER-IN-PLACE over a corrupt restoreFrom snapshot: salvage local-only writes + DLQ, refold fresh
904
934
  // from cloud, re-offer the salvaged work. The report lands on [lastRecovery]. Nothing local is lost.
905
935
  if (recover) 'recover': true,
936
+ // CUSTODY-BREAK POLICY (rebirth_and_custody_lifecycle.md): "adopt"/"reject" decide automatically;
937
+ // "ask" surfaces the break on [custodyEvents] (kind "ask") and awaits [resolveCustodyBreak]. Omitted ⇒
938
+ // fail closed (the break surfaces as an error on the sync/pull that hit it; the holon stays local).
939
+ if (onCustodyBreak != null) 'onCustodyBreak': onCustodyBreak.wire,
906
940
  }));
907
941
  _lastRecovery = (r['recovery'] as Map?)?.cast<String, Object?>();
908
942
  }
909
943
 
944
+ /// The custody lifecycle stream (rebirth_and_custody_lifecycle.md) — a rebirth/divergence surfaces here as a
945
+ /// typed [NomosCustodyEvent]. Bind a Flutter break dialog / posture banner to it; for a `CustodyBreakPolicy.
946
+ /// ask` session, an event of `kind == 'ask'` awaits [resolveCustodyBreak].
947
+ Stream<NomosCustodyEvent> get custodyEvents => _custodyEvents.stream;
948
+
949
+ /// Where a session sits on the custody spectrum (incarnation, posture, pending/salvaged local work,
950
+ /// supersession history) — for a status banner or a "you're offline on old truth" indicator.
951
+ Future<NomosCustodyStatus> custodyStatus({String? session}) async =>
952
+ NomosCustodyStatus.fromJson(
953
+ _asMap(await _call('custodyStatus', {if (session != null) 'session': session})));
954
+
955
+ /// Continue an `ask`-policy custody break with the user's decision (adopt|reject) — the async UX lane.
956
+ Future<Map<String, Object?>> resolveCustodyBreak(String decision,
957
+ {String? session}) async =>
958
+ _asMap(await _call('resolveCustodyBreak', {
959
+ 'decision': decision,
960
+ if (session != null) 'session': session,
961
+ }));
962
+
963
+ /// The parked salvaged intents (re-offerable local work an `adopt` rescued) — metadata only.
964
+ Future<List<Map<String, Object?>>> salvagedIntents({String? session}) async {
965
+ final r = _asMap(await _call('salvagedIntents', {if (session != null) 'session': session}));
966
+ return ((r['salvaged'] as List?) ?? const [])
967
+ .map((e) => (e as Map).cast<String, Object?>())
968
+ .toList();
969
+ }
970
+
971
+ /// Re-offer parked salvaged work under the current (adopted) law — the explicit app move (never automatic
972
+ /// across incarnations). [id] re-offers one; omitted re-offers all.
973
+ Future<Map<String, Object?>> reofferSalvaged({String? id, String? session}) async =>
974
+ _asMap(await _call('reofferSalvaged',
975
+ {if (id != null) 'id': id, if (session != null) 'session': session}));
976
+
977
+ /// Discard one parked salvaged intent — the app's explicit choice.
978
+ Future<Map<String, Object?>> discardSalvaged(String id, {String? session}) async =>
979
+ _asMap(await _call('discardSalvaged', {'id': id, if (session != null) 'session': session}));
980
+
910
981
  /// The salvage report from the most recent `connect(recover: true)` — `{ salvagedWrites, requeuedDeadLetters,
911
982
  /// unsalvageable }`, or null if the connection didn't recover. Show "recovered N writes" / surface
912
983
  /// `unsalvageable` (writes the current law refused — kept in the DLQ, never dropped).
@@ -965,6 +1036,32 @@ class NomosBridge {
965
1036
  List<Map<String, Object?>>? attestations,
966
1037
  bool? force,
967
1038
  String? session,
1039
+ }) =>
1040
+ offerOutcome(
1041
+ domain: domain,
1042
+ directiveId: directiveId,
1043
+ payload: payload,
1044
+ actor: actor,
1045
+ domainHash: domainHash,
1046
+ attestations: attestations,
1047
+ force: force,
1048
+ session: session,
1049
+ );
1050
+
1051
+ /// THE ONE OUTCOME-RETURNING OFFER LANE — [offerDirective] answering the full typed [NomosOfferOutcome]
1052
+ /// (head + law-minted `created` NomosRefs + births), for EVERY generated method (create or mutate). A pure
1053
+ /// mutation returns `created: []`; a `.creates(...)` plan returns the minted aggregate id(s), each already
1054
+ /// carrying the session [workspace] so `outcome.created.first.ref` is a complete followable NomosRef with no
1055
+ /// query-after round-trip. [offerCreates] is the historical alias (identical behavior).
1056
+ Future<NomosOfferOutcome> offerOutcome({
1057
+ required String domain,
1058
+ required String directiveId,
1059
+ required Object? payload,
1060
+ String? actor,
1061
+ String? domainHash,
1062
+ List<Map<String, Object?>>? attestations,
1063
+ bool? force,
1064
+ String? session,
968
1065
  }) async {
969
1066
  final r = _asMap(await _call('offerDirective', {
970
1067
  'domain': domain,
@@ -992,13 +1089,17 @@ class NomosBridge {
992
1089
  .map((m) => (m['workspace'] ?? '').toString())
993
1090
  .where((s) => s.isNotEmpty),
994
1091
  ]..removeWhere((s) => s.isEmpty);
1092
+ // The offer ran on this workspace (NomosRef) — stamp every minted ref with it so it is a complete locator.
1093
+ final ws = (r['workspace'] ?? '').toString();
995
1094
  return NomosOfferOutcome(
996
1095
  head: r['head'].toString(),
1096
+ workspace: ws,
997
1097
  created: ((r['created'] as List?) ?? const [])
998
1098
  .whereType<Map>()
999
1099
  .map((m) => NomosCreated(
1000
1100
  type: (m['type'] ?? '').toString(),
1001
- id: (m['id'] ?? '').toString()))
1101
+ id: (m['id'] ?? '').toString(),
1102
+ workspace: ws))
1002
1103
  .toList(),
1003
1104
  born: bornNames,
1004
1105
  birthOutcomes: births,
@@ -1669,6 +1770,7 @@ class NomosBridge {
1669
1770
  _stopStatusPoll();
1670
1771
  if (!_syncEvents.isClosed) await _syncEvents.close();
1671
1772
  if (!_signalEvents.isClosed) await _signalEvents.close();
1773
+ if (!_custodyEvents.isClosed) await _custodyEvents.close();
1672
1774
  if (!_flameEvents.isClosed) await _flameEvents.close();
1673
1775
  if (!_spanEvents.isClosed) await _spanEvents.close();
1674
1776
  if (!_transferEvents.isClosed) await _transferEvents.close();
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.1.0"
194
+ version: "0.1.2"
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.44.0
14
+ version: 0.46.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.1.2
23
+ nomos_types: ^0.1.4
24
24
  json_schema: ^5.2.2
25
25
  crypto: ^3.0.0
26
26
 
@@ -1,3 +1,16 @@
1
+ ## 0.1.4
2
+
3
+ - Custody-lifecycle types: `NomosCustodyBreak`, `NomosCustodyStatus`, `NomosCustodyEvent`,
4
+ `CustodyBreakPolicy` (adopt/reject/ask) — a workspace rebirth surfaces as a typed, app-decidable event.
5
+ Purely additive.
6
+
7
+ ## 0.1.3
8
+
9
+ - `NomosRef` — THE one typed reference `{workspace, type, id}`: value semantics, the canonical
10
+ `nomos://<workspace>/<type>/<id>` uri (`uri`/`parse`, encode round-trip), `fromJson` (object or uri),
11
+ `resolve()` over the multi-workspace reader. `ActivityRef` gains `workspace` + a `.ref` view; `ForeignRef`
12
+ gains `.ref`. Every reference shape now converges on the one triple. Purely additive.
13
+
1
14
  ## 0.1.2
2
15
 
3
16
  - Activity-feed runtime types: `ActivityEvent` (the shared activity row — kind/actor/occurredAt, rendered
@@ -32,3 +32,7 @@ export 'src/errors.dart';
32
32
  // abstract interfaces (no bridge) — so both the layered TYPES package and nomos_client resolve them.
33
33
  export 'src/foreign_ref.dart';
34
34
  export 'src/activity.dart';
35
+ export 'src/nomos_ref.dart';
36
+ // The CUSTODY-LIFECYCLE types (rebirth_and_custody_lifecycle.md): NomosCustodyBreak / NomosCustodyStatus /
37
+ // CustodyBreakPolicy — custody incompatibility as a first-class, typed, app-decidable event. Pure data.
38
+ export 'src/custody.dart';
@@ -8,22 +8,35 @@
8
8
  // the generated pure `renderActivity` lives) AND the runtime `nomos_client` package resolve them.
9
9
  library;
10
10
 
11
+ import 'nomos_ref.dart';
12
+
11
13
  /// A declared navigation target on an activity row: the referenced aggregate/contract TYPE + the resolved id
12
14
  /// (read from a payload field the directive named). Replaces the old enricher's blind id-probing — the app
13
15
  /// knows exactly what `id` points at and can deep-link/resolve it.
14
16
  class ActivityRef {
15
17
  final String type;
16
18
  final String id;
17
- const ActivityRef({required this.type, required this.id});
18
19
 
19
- factory ActivityRef.fromJson(Map<String, dynamic> j) =>
20
- ActivityRef(type: (j['type'] ?? '') as String, id: (j['id'] ?? '') as String);
21
- Map<String, dynamic> toJson() => {'type': type, 'id': id};
20
+ /// The source workspace (additive — empty on rows rendered before refs carried it).
21
+ final String workspace;
22
+
23
+ const ActivityRef({required this.type, required this.id, this.workspace = ''});
24
+
25
+ /// The canonical reference view (`nomos://<workspace>/<type>/<id>`).
26
+ NomosRef get ref => NomosRef(workspace: workspace, type: type, id: id);
27
+
28
+ factory ActivityRef.fromJson(Map<String, dynamic> j) => ActivityRef(
29
+ type: (j['type'] ?? '') as String,
30
+ id: (j['id'] ?? '') as String,
31
+ workspace: (j['workspace'] ?? '') as String);
32
+ Map<String, dynamic> toJson() =>
33
+ {'type': type, 'id': id, if (workspace.isNotEmpty) 'workspace': workspace};
22
34
 
23
35
  @override
24
- bool operator ==(Object other) => other is ActivityRef && other.type == type && other.id == id;
36
+ bool operator ==(Object other) =>
37
+ other is ActivityRef && other.type == type && other.id == id && other.workspace == workspace;
25
38
  @override
26
- int get hashCode => Object.hash(type, id);
39
+ int get hashCode => Object.hash(type, id, workspace);
27
40
  }
28
41
 
29
42
  /// Row prominence — a declared default the app may style or threshold on.
@@ -0,0 +1,203 @@
1
+ // NOMOS — the CUSTODY-LIFECYCLE types (rebirth_and_custody_lifecycle.md, the client half). Custody
2
+ // incompatibility is a FIRST-CLASS, app-decidable event: a rebirth (rebirthWorkspace) re-genesises a
3
+ // workspace's chain, invalidating every client's local history. The client detects the break, names it
4
+ // truthfully, and hands the app the decision — it NEVER silently adopts foreign truth nor loses local work.
5
+ //
6
+ // Pure data (no bridge/lifecycle/Flutter) so BOTH the layered TYPES package and the runtime `nomos_client`
7
+ // resolve them. The bridge decodes these from the runner's `custody` events + `custodyStatus` reply.
8
+ library;
9
+
10
+ /// WHY a chain was superseded — the lineage annotation lifted from the workspace status onto a break.
11
+ class NomosCustodySupersession {
12
+ final String? reason;
13
+ final String? supersededAt;
14
+
15
+ const NomosCustodySupersession({this.reason, this.supersededAt});
16
+
17
+ factory NomosCustodySupersession.fromJson(Map<String, dynamic> j) =>
18
+ NomosCustodySupersession(
19
+ reason: j['reason'] as String?,
20
+ supersededAt: j['supersededAt'] as String?,
21
+ );
22
+ Map<String, dynamic> toJson() =>
23
+ {'reason': reason, 'supersededAt': supersededAt};
24
+ }
25
+
26
+ /// The kind of custody break.
27
+ /// - [superseded]: a LAWFUL rebirth — the remote incarnation differs and the lineage records the
28
+ /// supersession (a reason/attribution rides the break).
29
+ /// - [diverged]: the histories don't reconcile with NO supersession explanation (an unrelated/foreign chain).
30
+ enum NomosCustodyBreakKind {
31
+ superseded,
32
+ diverged;
33
+
34
+ static NomosCustodyBreakKind fromWire(String? s) =>
35
+ s == 'diverged' ? NomosCustodyBreakKind.diverged : NomosCustodyBreakKind.superseded;
36
+ String get wire => this == NomosCustodyBreakKind.diverged ? 'diverged' : 'superseded';
37
+ }
38
+
39
+ /// A CUSTODY BREAK — the remote incarnation is incompatible with this client's local chain. Carries
40
+ /// everything the app needs to choose adopt vs reject (and to explain WHY to a user).
41
+ class NomosCustodyBreak {
42
+ final NomosCustodyBreakKind kind;
43
+
44
+ /// This client's local chain genesis oid.
45
+ final String localGenesis;
46
+
47
+ /// The remote serving chain's genesis oid (the workspace's current incarnation).
48
+ final String remoteGenesis;
49
+ final String remoteIncarnation;
50
+
51
+ /// Present for a [NomosCustodyBreakKind.superseded] break — why the old chain was retired.
52
+ final NomosCustodySupersession? supersession;
53
+
54
+ /// How many local-only intents (un-synced authored work) are at stake — never lost by any policy.
55
+ final int localOnlyIntents;
56
+
57
+ /// Epoch-ms of the last successful converge before the break (or null if never synced).
58
+ final int? lastSyncedAt;
59
+
60
+ const NomosCustodyBreak({
61
+ required this.kind,
62
+ required this.localGenesis,
63
+ required this.remoteGenesis,
64
+ required this.remoteIncarnation,
65
+ this.supersession,
66
+ this.localOnlyIntents = 0,
67
+ this.lastSyncedAt,
68
+ });
69
+
70
+ factory NomosCustodyBreak.fromJson(Map<String, dynamic> j) => NomosCustodyBreak(
71
+ kind: NomosCustodyBreakKind.fromWire(j['kind'] as String?),
72
+ localGenesis: (j['localGenesis'] ?? '') as String,
73
+ remoteGenesis: (j['remoteGenesis'] ?? j['remoteIncarnation'] ?? '') as String,
74
+ remoteIncarnation: (j['remoteIncarnation'] ?? j['remoteGenesis'] ?? '') as String,
75
+ supersession: (j['supersession'] is Map)
76
+ ? NomosCustodySupersession.fromJson(
77
+ (j['supersession'] as Map).cast<String, dynamic>())
78
+ : null,
79
+ localOnlyIntents: (j['localOnlyIntents'] as num?)?.toInt() ?? 0,
80
+ lastSyncedAt: (j['lastSyncedAt'] as num?)?.toInt(),
81
+ );
82
+
83
+ Map<String, dynamic> toJson() => {
84
+ 'kind': kind.wire,
85
+ 'localGenesis': localGenesis,
86
+ 'remoteGenesis': remoteGenesis,
87
+ 'remoteIncarnation': remoteIncarnation,
88
+ if (supersession != null) 'supersession': supersession!.toJson(),
89
+ 'localOnlyIntents': localOnlyIntents,
90
+ 'lastSyncedAt': lastSyncedAt,
91
+ };
92
+ }
93
+
94
+ /// The app's decision for a custody break (the declared or interactive policy outcome).
95
+ enum CustodyBreakPolicy {
96
+ /// Salvage local-only work, re-clone the new incarnation, park the salvage re-offerable, resume.
97
+ adopt,
98
+
99
+ /// Stay on local truth, go offline-only (the local holon keeps serving; sync suspends).
100
+ reject,
101
+
102
+ /// Surface the break on [custodyEvents] and await `resolveCustodyBreak(...)` (async UX — a dialog).
103
+ ask;
104
+
105
+ String get wire => name;
106
+ }
107
+
108
+ /// A supersession entry from the workspace lineage (custody introspection history).
109
+ class NomosSupersededChain {
110
+ final String? ref;
111
+ final String? supersededAt;
112
+ final String? reason;
113
+
114
+ const NomosSupersededChain({this.ref, this.supersededAt, this.reason});
115
+
116
+ factory NomosSupersededChain.fromJson(Map<String, dynamic> j) => NomosSupersededChain(
117
+ ref: j['ref'] as String?,
118
+ supersededAt: j['supersededAt'] as String?,
119
+ reason: j['reason'] as String?,
120
+ );
121
+ Map<String, dynamic> toJson() =>
122
+ {'ref': ref, 'supersededAt': supersededAt, 'reason': reason};
123
+ }
124
+
125
+ /// A custody lifecycle event pushed over the bridge (mirrors syncEvents). `kind` is one of
126
+ /// "detected" | "rejected" | "adopting" | "adopted" | "ask"; "ask" awaits `resolveCustodyBreak`.
127
+ class NomosCustodyEvent {
128
+ final String kind;
129
+ final String? session;
130
+ final NomosCustodyBreak? break_;
131
+ final bool? recloned;
132
+ final int? parkedIntents;
133
+
134
+ const NomosCustodyEvent({
135
+ required this.kind,
136
+ this.session,
137
+ this.break_,
138
+ this.recloned,
139
+ this.parkedIntents,
140
+ });
141
+
142
+ /// True when this event is an interactive prompt awaiting a decision.
143
+ bool get isAsk => kind == 'ask';
144
+
145
+ static NomosCustodyEvent? fromMessage(Map<String, dynamic> m) {
146
+ final c = (m['custody'] as Map?)?.cast<String, dynamic>();
147
+ if (c == null) return null;
148
+ final b = (c['break'] as Map?)?.cast<String, dynamic>();
149
+ return NomosCustodyEvent(
150
+ kind: (c['kind'] ?? '') as String,
151
+ session: m['session'] as String?,
152
+ break_: b != null ? NomosCustodyBreak.fromJson(b) : null,
153
+ recloned: c['recloned'] as bool?,
154
+ parkedIntents: (c['parkedIntents'] as num?)?.toInt(),
155
+ );
156
+ }
157
+ }
158
+
159
+ /// Where a holon sits on the custody spectrum ([custodyStatus]).
160
+ class NomosCustodyStatus {
161
+ /// The chain THIS holon serves (its own genesis oid).
162
+ final String? incarnation;
163
+ final String? localGenesis;
164
+
165
+ /// The remote serving chain's genesis (or null when unknown / pre-rebirth cloud).
166
+ final String? remoteIncarnation;
167
+
168
+ /// One of: synced | custody-break | custody-break-rejected | adopting.
169
+ final String syncPosture;
170
+ final int pendingLocalIntents;
171
+ final int salvagedIntents;
172
+ final List<NomosSupersededChain> supersessionHistory;
173
+
174
+ /// The unresolved break, when the holon is sitting in a break posture.
175
+ final NomosCustodyBreak? pendingBreak;
176
+
177
+ const NomosCustodyStatus({
178
+ this.incarnation,
179
+ this.localGenesis,
180
+ this.remoteIncarnation,
181
+ this.syncPosture = 'synced',
182
+ this.pendingLocalIntents = 0,
183
+ this.salvagedIntents = 0,
184
+ this.supersessionHistory = const [],
185
+ this.pendingBreak,
186
+ });
187
+
188
+ factory NomosCustodyStatus.fromJson(Map<String, dynamic> j) => NomosCustodyStatus(
189
+ incarnation: j['incarnation'] as String?,
190
+ localGenesis: j['localGenesis'] as String?,
191
+ remoteIncarnation: j['remoteIncarnation'] as String?,
192
+ syncPosture: (j['syncPosture'] ?? 'synced') as String,
193
+ pendingLocalIntents: (j['pendingLocalIntents'] as num?)?.toInt() ?? 0,
194
+ salvagedIntents: (j['salvagedIntents'] as num?)?.toInt() ?? 0,
195
+ supersessionHistory: ((j['supersessionHistory'] as List?) ?? const [])
196
+ .whereType<Map>()
197
+ .map((m) => NomosSupersededChain.fromJson(m.cast<String, dynamic>()))
198
+ .toList(),
199
+ pendingBreak: (j['pendingBreak'] is Map)
200
+ ? NomosCustodyBreak.fromJson((j['pendingBreak'] as Map).cast<String, dynamic>())
201
+ : null,
202
+ );
203
+ }
@@ -14,6 +14,8 @@
14
14
  // this barrel, and its `NomosBridge` implements [NomosForeignReader] over the device's multi-workspace plane.
15
15
  library;
16
16
 
17
+ import 'nomos_ref.dart';
18
+
17
19
  /// Reads a FOREIGN workspace on demand — the surface a [ForeignRef] resolves against. It is STRUCTURAL: any
18
20
  /// object with this shape works. `nomos_client`'s `NomosBridge` implements it over the device's local
19
21
  /// multi-workspace plane (`bridge.session(ws).queryById(id)`, mounting the workspace lazily); the TS twin is
@@ -111,6 +113,10 @@ class ForeignRef<T> {
111
113
  /// The foreign aggregate type this locator points at.
112
114
  final String type;
113
115
 
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);
119
+
114
120
  /// The target's decoder (the aggregate `fromJson` factory), applied to a resolved row's flat `data` map.
115
121
  final T Function(Map<String, dynamic> data, [String id]) _decode;
116
122
 
@@ -0,0 +1,75 @@
1
+ // NOMOS — THE ONE REFERENCE (architecture/nomos_ref.md). A [NomosRef] is the canonical, globally-unique,
2
+ // workspace-qualified identity of an aggregate instance: `{ workspace, type, id }`, with the stable string
3
+ // form `nomos://<workspace>/<type>/<id>`. It travels WITH the data by default — every generated read row,
4
+ // write outcome, and activity ref carries (or views as) one — so a dev holding a thing can always point at
5
+ // it: no client-side UUID pre-minting, no loose {workspaceId, id} string pairs, no re-query-and-diff.
6
+ //
7
+ // A NomosRef is a LOCATOR (like ForeignRef, which remains the TYPED per-field view): `resolve` is an
8
+ // informational read over the device's multi-workspace plane; law-grade attestation stays on the declared
9
+ // attested-read lane. Pure nomos_types — no bridge/lifecycle imports.
10
+ library;
11
+
12
+ import 'foreign_ref.dart';
13
+
14
+ /// The canonical typed reference to an aggregate instance: workspace + aggregate type + instance id.
15
+ /// Value semantics; `uri` is the stable interop string (`nomos://<workspace>/<type>/<id>`).
16
+ class NomosRef {
17
+ /// The workspace holding the instance.
18
+ final String workspace;
19
+
20
+ /// The aggregate TYPE (the read-projection type tag, e.g. `estate.EstateSite` or `EstateSite`).
21
+ final String type;
22
+
23
+ /// The instance id (law-minted or law-defined).
24
+ final String id;
25
+
26
+ const NomosRef({required this.workspace, required this.type, required this.id});
27
+
28
+ /// The stable string form — survives URLs, nav params, JSON, logs. Segments are URI-encoded so a
29
+ /// workspace/type/id containing `/` or spaces round-trips.
30
+ String get uri =>
31
+ 'nomos://${Uri.encodeComponent(workspace)}/${Uri.encodeComponent(type)}/${Uri.encodeComponent(id)}';
32
+
33
+ /// Parse a `nomos://<workspace>/<type>/<id>` string (the inverse of [uri]). Throws [FormatException]
34
+ /// on anything else.
35
+ factory NomosRef.parse(String uri) {
36
+ final u = Uri.tryParse(uri);
37
+ if (u == null || u.scheme != 'nomos') {
38
+ throw FormatException('NomosRef.parse: not a nomos:// uri', uri);
39
+ }
40
+ // nomos://<ws>/<type>/<id> — the workspace lands as the authority (host), type/id as path segments.
41
+ final ws = Uri.decodeComponent(u.host);
42
+ final segs = u.pathSegments.map(Uri.decodeComponent).toList();
43
+ if (ws.isEmpty || segs.length != 2 || segs[0].isEmpty || segs[1].isEmpty) {
44
+ throw FormatException('NomosRef.parse: expected nomos://<workspace>/<type>/<id>', uri);
45
+ }
46
+ return NomosRef(workspace: ws, type: segs[0], id: segs[1]);
47
+ }
48
+
49
+ /// Accepts the `{workspace,type,id}` object OR the `nomos://` uri string.
50
+ factory NomosRef.fromJson(Object? j) {
51
+ if (j is String) return NomosRef.parse(j);
52
+ final m = (j as Map).cast<String, dynamic>();
53
+ return NomosRef(
54
+ workspace: (m['workspace'] ?? '') as String,
55
+ type: (m['type'] ?? '') as String,
56
+ id: (m['id'] ?? '') as String,
57
+ );
58
+ }
59
+
60
+ Map<String, dynamic> toJson() => {'workspace': workspace, 'type': type, 'id': id};
61
+
62
+ /// Informational lookup of the referenced instance over the device's multi-workspace plane (the same
63
+ /// reader `ForeignRef.resolve` uses — `bridge.foreignReader()` in nomos_client). Returns the raw
64
+ /// `{type,id,data}` rows for the id in the ref's workspace (empty ⇒ not found / not visible).
65
+ Future<List<Map<String, dynamic>>> resolve(NomosForeignReader nomos) =>
66
+ nomos.foreignWorkspace(workspace).queryById(id);
67
+
68
+ @override
69
+ bool operator ==(Object other) =>
70
+ other is NomosRef && other.workspace == workspace && other.type == type && other.id == id;
71
+ @override
72
+ int get hashCode => Object.hash(workspace, type, id);
73
+ @override
74
+ String toString() => uri;
75
+ }
@@ -11,7 +11,7 @@ description: >-
11
11
  Pure kernel wire/data types for Nomos Dart frontends — no transport, no bridge,
12
12
  no runtime. The hand-written serde-JSON-compatible types plus the offer codec,
13
13
  reactive read engine, schema validation, and data-only error/trace types.
14
- version: 0.1.2
14
+ version: 0.1.4
15
15
 
16
16
  environment:
17
17
  sdk: ^3.5.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.67.0",
3
+ "version": "0.69.0",
4
4
  "type": "module",
5
5
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",