@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 +14 -0
- package/dart/lib/src/webview_bridge.dart +105 -3
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart_types/CHANGELOG.md +13 -0
- package/dart_types/lib/nomos_types.dart +4 -0
- package/dart_types/lib/src/activity.dart +19 -6
- package/dart_types/lib/src/custody.dart +203 -0
- package/dart_types/lib/src/foreign_ref.dart +6 -0
- package/dart_types/lib/src/nomos_ref.dart +75 -0
- package/dart_types/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/src/activity.ts +22 -4
- package/src/codegen_dart.ts +45 -6
- package/src/codegen_dart_client.ts +32 -43
- package/src/codegen_ts.ts +89 -62
- package/src/compile_package_main.ts +8 -4
- package/src/directive.ts +7 -2
- package/src/framework/workspaces.ts +133 -1
- package/src/index.ts +3 -0
- package/src/repair.ts +151 -0
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
|
-
|
|
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
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.
|
|
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.
|
|
23
|
+
nomos_types: ^0.1.4
|
|
24
24
|
json_schema: ^5.2.2
|
|
25
25
|
crypto: ^3.0.0
|
|
26
26
|
|
package/dart_types/CHANGELOG.md
CHANGED
|
@@ -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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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) =>
|
|
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
|
+
}
|
package/dart_types/pubspec.yaml
CHANGED
|
@@ -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.
|
|
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.
|
|
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",
|