@githolon/dsl 0.14.0 → 0.16.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/example/nomos_webview_transport.dart +2 -2
- package/dart/lib/nomos_dsl.dart +4 -4
- package/dart/lib/src/{dispatch.dart → offer.dart} +7 -7
- package/dart/lib/src/provider.dart +30 -30
- package/dart/lib/src/subscriptions.dart +3 -3
- package/dart/lib/src/webview_bridge.dart +56 -21
- package/dart/test/webview_bridge_test.dart +35 -34
- package/package.json +5 -3
- package/src/aggregate.ts +9 -1
- package/src/authoring.ts +10 -1
- package/src/build_package.ts +1 -1
- package/src/capability_exports.ts +2 -2
- package/src/codegen_dart.ts +34 -24
- package/src/codegen_dart_client.ts +74 -12
- package/src/codegen_provider_dart.ts +67 -61
- package/src/codegen_ts.ts +45 -17
- package/src/compile_package_main.ts +103 -36
- package/src/ctx.ts +13 -0
- package/src/engine_entry.ts +22 -5
- package/src/fields.ts +44 -0
- package/src/flow_gate.ts +208 -0
- package/src/framework/capability.ts +29 -11
- package/src/framework/encryption_scope.ts +9 -1
- package/src/framework/genesis.ts +5 -1
- package/src/framework/impure_capability.ts +73 -67
- package/src/framework/relations.ts +23 -4
- package/src/framework/signing_identity.ts +1 -1
- package/src/framework/sync_lifecycle.ts +3 -3
- package/src/framework/workspace_invariant.ts +29 -8
- package/src/framework/workspaces.ts +7 -0
- package/src/index.ts +25 -1
- package/src/manifest.ts +11 -17
- package/src/ops.ts +42 -1
- package/src/principal_evidence.ts +112 -0
- package/src/relation_sugar.ts +10 -4
- package/src/shape_compat.ts +208 -0
- package/src/usd.ts +287 -0
- package/src/usd_layers.ts +213 -26
- package/src/wire.ts +8 -0
- package/src/wire_encode.ts +75 -2
- package/src/workspace_sharding.ts +197 -24
- package/src/workspace_type.ts +1 -0
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
// final bridge = NomosBridge(transport);
|
|
16
16
|
// await bridge.ready; // resolves on the runner's {event:ready}
|
|
17
17
|
// await bridge.connect(cloud: 'https://nomos.captainapp.co.uk', workspace: myWs, clientId: deviceId);
|
|
18
|
-
// final
|
|
19
|
-
//
|
|
18
|
+
// final app = GuestbookClient(bridge);
|
|
19
|
+
// final head = await app.signGuestbook(SignGuestbookPayload(author: 'ada', message: 'hi', mood: 'delighted'));
|
|
20
20
|
// final rows = await bridge.query('entriesByAuthor', {'author': 'ada'});
|
|
21
21
|
//
|
|
22
22
|
// (Put the runner bundle at `assets/nomos/nomos-runner.{html,js}` and declare it under `flutter: assets:`.)
|
package/dart/lib/nomos_dsl.dart
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
/// - The hand-written kernel WIRE types (`Value`, `Field`/`WireField`, `Hlc`,
|
|
11
11
|
/// `Op`, `FieldOp`, `Event`, `Schema`/`WireDriver`) — byte-compatible with
|
|
12
12
|
/// serde_json. See `src/wire.dart`, `src/driver.dart`.
|
|
13
|
-
/// - The ONE write verb `
|
|
13
|
+
/// - The ONE effectful write verb `offer(intent)` (`src/offer.dart`) + the typed
|
|
14
14
|
/// reactive read engine `NomosReads` (`src/subscriptions.dart`).
|
|
15
15
|
/// - The framework PRIMITIVES ONLY. The CODE-GENERATED per-domain types
|
|
16
16
|
/// (aggregates + public intent payloads + enums + `watch…` read accessors) NO LONGER
|
|
@@ -18,14 +18,14 @@
|
|
|
18
18
|
/// `co2_nomos_domains` (`ts_packages/co2-nomos-domains/dart/`), emitted there by
|
|
19
19
|
/// `ts_packages/co2-nomos-domains/src/emit_dart.ts`. Tenant domains squatting in
|
|
20
20
|
/// the framework package was the wrong topology; this barrel now exports ONLY the
|
|
21
|
-
/// hand-written framework wire/
|
|
21
|
+
/// hand-written framework wire/offer/read/provider primitives. App code that
|
|
22
22
|
/// wants the typed domain surface imports `package:co2_nomos_domains/co2_nomos_domains.dart`,
|
|
23
23
|
/// which re-exports this framework barrel PLUS every generated domain.
|
|
24
24
|
library;
|
|
25
25
|
|
|
26
26
|
export 'src/wire.dart';
|
|
27
27
|
export 'src/driver.dart';
|
|
28
|
-
export 'src/
|
|
28
|
+
export 'src/offer.dart';
|
|
29
29
|
export 'src/subscriptions.dart';
|
|
30
30
|
export 'src/schema_validation.dart';
|
|
31
31
|
// THE WEBVIEW BRIDGE (app-binding boundary) — the async Dart runtime that drives the REAL githolon running
|
|
@@ -33,7 +33,7 @@ export 'src/schema_validation.dart';
|
|
|
33
33
|
export 'src/webview_bridge.dart';
|
|
34
34
|
// The PROVIDER boundary (server-side capability-provider SDK runtime: TaskOutcome /
|
|
35
35
|
// TaskSuccess / TaskFailure / TaskResult / TaskProvider / HostClock). The symmetric twin
|
|
36
|
-
// of `
|
|
36
|
+
// of `offer.dart`; the generated per-task provider classes build on it.
|
|
37
37
|
export 'src/provider.dart';
|
|
38
38
|
|
|
39
39
|
// ── NO generated domains here ──────────────────────────────────────────────
|
|
@@ -5,19 +5,19 @@
|
|
|
5
5
|
// wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
|
|
6
6
|
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
7
7
|
|
|
8
|
-
/// The ONE write verb: `
|
|
8
|
+
/// The ONE effectful write verb: `offer(intent)`.
|
|
9
9
|
///
|
|
10
|
-
/// END-STATE (the "
|
|
11
|
-
/// write verb is `
|
|
10
|
+
/// END-STATE (the "offer(intent) purge", Jack 2026-06-18): app code's only
|
|
11
|
+
/// effectful write verb is `offer(intent)`. The frontend constructs ONLY a typed
|
|
12
12
|
/// generated intent DTO (a [NomosIntent]) — it never hand-types an engine rule
|
|
13
|
-
/// id, never builds an `Event`, never holds an `Hlc`. `
|
|
13
|
+
/// id, never builds an `Event`, never holds an `Hlc`. `offer` ships the
|
|
14
14
|
/// intent payload as DATA to the kernel's FRB `author(domain, directiveId, payloadJson,
|
|
15
15
|
/// actor)`; the sealed engine runs the REAL `.plan()` and is the SOLE lowering
|
|
16
16
|
/// authority (HLC + intent id are kernel-allocated host-side).
|
|
17
17
|
///
|
|
18
18
|
/// This file is HAND-WRITTEN and domain-AGNOSTIC (declared ONCE, here): the
|
|
19
19
|
/// generated per-domain intent classes `implements NomosIntent`, so a single
|
|
20
|
-
/// generic `
|
|
20
|
+
/// generic `offer` covers every public intent of every domain — replacing the
|
|
21
21
|
/// ~63-method write façade.
|
|
22
22
|
library;
|
|
23
23
|
|
|
@@ -39,7 +39,7 @@ abstract interface class NomosIntent {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/// The kernel write seam: the FRB `NomosClient.author` signature, as a function
|
|
42
|
-
/// type. `
|
|
42
|
+
/// type. `offer` takes this (rather than importing the FRB client) so the
|
|
43
43
|
/// generated `nomos_dsl` package stays free of a `flutter_rust_bridge` dependency
|
|
44
44
|
/// — the app threads `client.author` in. Returns the new HEAD commit oid (hex).
|
|
45
45
|
typedef Authorer = String Function({
|
|
@@ -54,7 +54,7 @@ typedef Authorer = String Function({
|
|
|
54
54
|
/// intent object — nothing is hand-typed. Builds NO `Event` and NO `Hlc`: the
|
|
55
55
|
/// engine lowers. [actor] is the authenticated actor the caller threads from auth.
|
|
56
56
|
/// Returns the new HEAD commit oid (hex).
|
|
57
|
-
String
|
|
57
|
+
String offer(NomosIntent intent,
|
|
58
58
|
{required Authorer author, required String actor}) =>
|
|
59
59
|
author(
|
|
60
60
|
domain: intent.domain,
|
|
@@ -7,22 +7,22 @@
|
|
|
7
7
|
|
|
8
8
|
/// The PROVIDER boundary: the typed server-side capability-provider SDK runtime.
|
|
9
9
|
///
|
|
10
|
-
/// This is the SYMMETRIC TWIN of `
|
|
11
|
-
/// the peer authors an
|
|
10
|
+
/// This is the SYMMETRIC TWIN of `offer.dart` (the peer write boundary). Where
|
|
11
|
+
/// the peer authors an Obligation (`createTranscribeVoiceNote(...)` -> `offer`), a
|
|
12
12
|
/// PROVIDER (Whisper, a renderer, a multi-target PR transporter) is the
|
|
13
|
-
/// other end of the framework `impureCapability`
|
|
13
|
+
/// other end of the framework `impureCapability` Obligation→Receipt pair: it WATCHES `requested`
|
|
14
14
|
/// task rows, performs the impure side-effect OUTSIDE the fold, and authors the RECEIPT
|
|
15
15
|
/// (`complete…`/`fail…`/`block…`/`deadLetter…`) back through the SAME write path. The receipt is an ordinary
|
|
16
16
|
/// intent — the provider is a Nomos author, NOT a bypass (ONE-WRITE-PATH).
|
|
17
|
-
///
|
|
17
|
+
/// Obligation and Receipt are intent roles, not provider-owned jobs or privileged server
|
|
18
18
|
/// mutation channels.
|
|
19
19
|
///
|
|
20
20
|
/// PRINCIPLE (Jack 2026-06-04): code is ~free; INTEGRATION POINTS are expensive +
|
|
21
21
|
/// fuckup-guaranteed. So the generated provider SDK is fully TYPED to the boundary —
|
|
22
|
-
/// the dev writes a typed `handler(
|
|
23
|
-
/// dispatcher DECODES the incoming
|
|
22
|
+
/// the dev writes a typed `handler(Obligation) -> Result | Failure | Blocked | DeadLetter`; the generated
|
|
23
|
+
/// dispatcher DECODES the incoming Obligation JSON into the typed Obligation and ENCODES the typed
|
|
24
24
|
/// outcome into the ordinary Receipt intent. NO dynamic / `Map<String, dynamic>` / raw-JSON leaks
|
|
25
|
-
/// into the dev-facing API; the types stop ONLY at the actual JSON serde (`
|
|
25
|
+
/// into the dev-facing API; the types stop ONLY at the actual JSON serde (`handleObligation`).
|
|
26
26
|
///
|
|
27
27
|
/// This file is HAND-WRITTEN and domain-AGNOSTIC (declared ONCE, here): the generated
|
|
28
28
|
/// per-task provider classes build on these interfaces, so the round-trip mechanism is
|
|
@@ -31,9 +31,9 @@ library;
|
|
|
31
31
|
|
|
32
32
|
import 'dart:convert';
|
|
33
33
|
|
|
34
|
-
import '
|
|
34
|
+
import 'offer.dart' show NomosIntent;
|
|
35
35
|
|
|
36
|
-
/// The typed OUTCOME a provider handler returns for one
|
|
36
|
+
/// The typed OUTCOME a provider handler returns for one Obligation: success, failure, blocked,
|
|
37
37
|
/// or dead-lettered.
|
|
38
38
|
/// SEALED so a generated dispatcher's `switch` is EXHAUSTIVE — a new outcome variant is
|
|
39
39
|
/// a compile error at every dispatcher, never a silent fall-through. The mirror of the
|
|
@@ -44,7 +44,7 @@ sealed class TaskOutcome<R> {
|
|
|
44
44
|
|
|
45
45
|
/// The SUCCESS outcome: the typed [result] the handler produced. The generated
|
|
46
46
|
/// dispatcher maps this → the `complete…` Receipt directive (status:completed +
|
|
47
|
-
/// resultRef +
|
|
47
|
+
/// resultRef + closesObligation).
|
|
48
48
|
final class TaskSuccess<R> extends TaskOutcome<R> {
|
|
49
49
|
/// The typed result the handler produced (e.g. a transcription's `resultRef`).
|
|
50
50
|
final R result;
|
|
@@ -53,7 +53,7 @@ final class TaskSuccess<R> extends TaskOutcome<R> {
|
|
|
53
53
|
|
|
54
54
|
/// The FAILURE outcome: the [failureReason] the handler recorded. The generated
|
|
55
55
|
/// dispatcher maps this → the `fail…` Receipt directive (status:failed + failureReason +
|
|
56
|
-
///
|
|
56
|
+
/// closesObligation). A recorded failure is the terminal state — NOT a retry storm
|
|
57
57
|
/// (conformance X1's "declined is recorded" discipline at the task scale).
|
|
58
58
|
final class TaskFailure<R> extends TaskOutcome<R> {
|
|
59
59
|
/// Why the side-effect failed (recorded onto the task, never re-thrown into the fold).
|
|
@@ -63,13 +63,13 @@ final class TaskFailure<R> extends TaskOutcome<R> {
|
|
|
63
63
|
|
|
64
64
|
/// The BLOCKED outcome: the provider cannot proceed until an external condition changes.
|
|
65
65
|
/// The generated dispatcher maps this -> the `block...` Receipt directive
|
|
66
|
-
/// (status:blocked + blockedReason + nextAction +
|
|
67
|
-
///
|
|
66
|
+
/// (status:blocked + blockedReason + nextAction + closesObligation). This is terminal for this
|
|
67
|
+
/// obligation; recovery is a fresh explicit Obligation after the next action is satisfied.
|
|
68
68
|
final class TaskBlocked<R> extends TaskOutcome<R> {
|
|
69
69
|
/// Why the provider cannot proceed.
|
|
70
70
|
final String blockedReason;
|
|
71
71
|
|
|
72
|
-
/// What the host/human must do before a fresh
|
|
72
|
+
/// What the host/human must do before a fresh obligation can proceed.
|
|
73
73
|
final String nextAction;
|
|
74
74
|
|
|
75
75
|
const TaskBlocked({
|
|
@@ -78,11 +78,11 @@ final class TaskBlocked<R> extends TaskOutcome<R> {
|
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/// The DEAD-LETTER outcome: stop retrying this
|
|
81
|
+
/// The DEAD-LETTER outcome: stop retrying this obligation and park it in the DLQ. The generated
|
|
82
82
|
/// dispatcher maps this -> the `deadLetter...` Receipt directive
|
|
83
|
-
/// (status:deadLettered + deadLetterReason +
|
|
83
|
+
/// (status:deadLettered + deadLetterReason + closesObligation).
|
|
84
84
|
final class TaskDeadLetter<R> extends TaskOutcome<R> {
|
|
85
|
-
/// Why this
|
|
85
|
+
/// Why this obligation was moved to the DLQ.
|
|
86
86
|
final String deadLetterReason;
|
|
87
87
|
const TaskDeadLetter(this.deadLetterReason);
|
|
88
88
|
}
|
|
@@ -106,34 +106,34 @@ abstract interface class TaskResult {
|
|
|
106
106
|
typedef HostClock = int Function();
|
|
107
107
|
|
|
108
108
|
/// The minimal contract a generated per-task provider exposes. Generic in the typed
|
|
109
|
-
///
|
|
109
|
+
/// Obligation/Result; the generated subtype binds them. `handleObligation` is the ONE seam a host
|
|
110
110
|
/// loop calls per `requested` row.
|
|
111
111
|
abstract interface class TaskProvider<O, R> {
|
|
112
112
|
/// The framework `impureCapability` ids this provider is bound to (provenance — the host
|
|
113
113
|
/// can assert it is draining the right capability). Carried as DATA, never hand-typed.
|
|
114
|
-
String get
|
|
114
|
+
String get obligationDirectiveId;
|
|
115
115
|
String get completeDirectiveId;
|
|
116
116
|
String get failDirectiveId;
|
|
117
117
|
String get blockDirectiveId;
|
|
118
118
|
String get deadLetterDirectiveId;
|
|
119
119
|
|
|
120
|
-
/// DECODE an incoming
|
|
120
|
+
/// DECODE an incoming Obligation intent's payload JSON into the typed [O], run the
|
|
121
121
|
/// registered typed handler, and ENCODE the outcome into the ordinary Receipt intent — a
|
|
122
|
-
/// [NomosIntent] ready for the ONE write path (`
|
|
122
|
+
/// [NomosIntent] ready for the ONE write path (`offer`). `now` stamps the
|
|
123
123
|
/// terminal time. Decoding raw JSON happens HERE and only here; the handler the dev
|
|
124
124
|
/// writes sees ONLY the typed [O] and returns ONLY a typed [TaskOutcome].
|
|
125
|
-
NomosIntent
|
|
125
|
+
NomosIntent handleObligation(String obligationPayloadJson, {required HostClock now});
|
|
126
126
|
|
|
127
|
-
/// Convenience: decode an
|
|
128
|
-
/// the row's `data` map in hand from a read). Same path as [
|
|
129
|
-
NomosIntent
|
|
130
|
-
Map<String, Object?>
|
|
127
|
+
/// Convenience: decode an Obligation payload from an already-parsed map (the host may have
|
|
128
|
+
/// the row's `data` map in hand from a read). Same path as [handleObligation].
|
|
129
|
+
NomosIntent handleObligationMap(
|
|
130
|
+
Map<String, Object?> obligationPayload, {
|
|
131
131
|
required HostClock now,
|
|
132
132
|
});
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
/// Decode an
|
|
136
|
-
/// Shared so every generated provider decodes
|
|
135
|
+
/// Decode an Obligation payload JSON string to a map for a generated `…Obligation.fromJson`.
|
|
136
|
+
/// Shared so every generated provider decodes Obligations identically (the symmetry of the
|
|
137
137
|
/// client's `jsonEncode(toPayloadJson())`).
|
|
138
|
-
Map<String, Object?>
|
|
139
|
-
(jsonDecode(
|
|
138
|
+
Map<String, Object?> decodeObligationPayload(String obligationPayloadJson) =>
|
|
139
|
+
(jsonDecode(obligationPayloadJson) as Map<String, Object?>);
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
/// The READ half of the client boundary: typed, reactive subscriptions.
|
|
9
9
|
///
|
|
10
|
-
/// END-STATE (the "
|
|
11
|
-
/// Nomos surface is `
|
|
10
|
+
/// END-STATE (the "offer(intent) purge", Jack 2026-06-18): the app's ONLY
|
|
11
|
+
/// Nomos surface is `offer(intent)` (write — see `offer.dart`) + TYPED
|
|
12
12
|
/// PUB/SUB SUBSCRIPTIONS (read — here + the generated per-domain accessors). The
|
|
13
13
|
/// app reads ONLY through generated typed accessors (`watchSites()`,
|
|
14
14
|
/// `watchSite(id)`, indexed-field queries → typed results); it never hand-writes a
|
|
@@ -75,7 +75,7 @@ typedef Summer = int Function(String sumId, String groupKey);
|
|
|
75
75
|
/// A "projection changed" tick: fires (any value) whenever a write may have
|
|
76
76
|
/// advanced the projection, so the subscription re-runs its indexed read. The
|
|
77
77
|
/// `query()`/`query_by_id()` closures are SYNCHRONOUS point-in-time reads; this tick is what
|
|
78
|
-
/// makes a typed accessor REACTIVE. The app threads its existing post-`
|
|
78
|
+
/// makes a typed accessor REACTIVE. The app threads its existing post-`offer`
|
|
79
79
|
/// invalidation signal in here (today the runtime's ~750ms projection poll / the
|
|
80
80
|
/// write-completion broadcast). A read with no live tick is a one-shot current read.
|
|
81
81
|
typedef ReadTick = Stream<void>;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
/// stable message protocol, so the generated typed Dart client drives the real kernel — never a
|
|
9
9
|
/// `MemoryPersistence` pretending to be a GitHolon.
|
|
10
10
|
///
|
|
11
|
-
/// A WebView round-trip is ASYNC, so the bridge is async (unlike the sync FRB `Authorer` in `
|
|
11
|
+
/// A WebView round-trip is ASYNC, so the bridge is async (unlike the sync FRB `Authorer` in `offer.dart`).
|
|
12
12
|
/// The generated per-domain Dart client wraps a [NomosBridge]; the bridge owns request/reply correlation +
|
|
13
13
|
/// reactive `watch` streams. The platform TRANSPORT ([BridgeTransport]) is injected — `dart:js_util` on web,
|
|
14
14
|
/// a WebView JS-channel on mobile — so this protocol logic is pure Dart + fully unit-testable.
|
|
@@ -41,6 +41,8 @@ class NomosBridge {
|
|
|
41
41
|
final BridgeTransport _transport;
|
|
42
42
|
final Map<int, Completer<Object?>> _pending = {};
|
|
43
43
|
final Map<String, StreamController<List<dynamic>>> _watches = {};
|
|
44
|
+
// SHAPE COMPATIBILITY (#72): compat bindings emit a verdict MAP, not a row list.
|
|
45
|
+
final Map<String, StreamController<Map<String, Object?>>> _compatWatches = {};
|
|
44
46
|
final StreamController<Map<String, Object?>> _syncEvents =
|
|
45
47
|
StreamController<Map<String, Object?>>.broadcast();
|
|
46
48
|
final StreamController<Map<String, Object?>> _signalEvents =
|
|
@@ -79,6 +81,17 @@ class NomosBridge {
|
|
|
79
81
|
} else {
|
|
80
82
|
ctrl.add((m['rows'] as List?) ?? const []);
|
|
81
83
|
}
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
// SHAPE COMPATIBILITY (#72): the compat binding emits a verdict OBJECT, not a row list.
|
|
87
|
+
final cctrl = _compatWatches[m['subId']];
|
|
88
|
+
if (cctrl != null && !cctrl.isClosed) {
|
|
89
|
+
if (m['ok'] == false) {
|
|
90
|
+
cctrl.addError(NomosBridgeError(
|
|
91
|
+
(m['error'] ?? 'the compat watch failed').toString()));
|
|
92
|
+
} else {
|
|
93
|
+
cctrl.add(((m['rows'] as Map?)?.cast<String, Object?>()) ?? const {});
|
|
94
|
+
}
|
|
82
95
|
}
|
|
83
96
|
return;
|
|
84
97
|
}
|
|
@@ -153,26 +166,9 @@ class NomosBridge {
|
|
|
153
166
|
});
|
|
154
167
|
}
|
|
155
168
|
|
|
156
|
-
/// LOCAL-FIRST
|
|
157
|
-
Future<String>
|
|
158
|
-
|
|
159
|
-
required String directiveId,
|
|
160
|
-
required Map<String, Object?> payload,
|
|
161
|
-
required String domainHash}) async {
|
|
162
|
-
final r = _asMap(await _call('dispatch', {
|
|
163
|
-
'domain': domain,
|
|
164
|
-
'directiveId': directiveId,
|
|
165
|
-
'payload': payload,
|
|
166
|
-
'domainHash': domainHash
|
|
167
|
-
}));
|
|
168
|
-
return r['head'].toString();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/// §5 cross-custody delivery: a `recordShare` carrying a pre-signed §3 quote into a home holon.
|
|
172
|
-
Future<String> deliverShare(
|
|
173
|
-
{required Map<String, Object?> quote, required String domainHash}) async {
|
|
174
|
-
final r = _asMap(await _call(
|
|
175
|
-
'deliverShare', {'quote': quote, 'domainHash': domainHash}));
|
|
169
|
+
/// LOCAL-FIRST offer: carry opaque intent bytes into the GitHolon. Returns the new local head; durable on [sync].
|
|
170
|
+
Future<String> offer({required String intentBytes}) async {
|
|
171
|
+
final r = _asMap(await _call('offer', {'intentBytes': intentBytes}));
|
|
176
172
|
return r['head'].toString();
|
|
177
173
|
}
|
|
178
174
|
|
|
@@ -261,6 +257,41 @@ class NomosBridge {
|
|
|
261
257
|
Stream<List<dynamic>> watchById(String id) =>
|
|
262
258
|
_watchStream(kind: 'byId', id: id);
|
|
263
259
|
|
|
260
|
+
/// SHAPE COMPATIBILITY (#72): one-shot verdict for this client's structural [requirement]
|
|
261
|
+
/// (its baked `…InterfaceJson`, decoded) against the workspace's ON-LEDGER offer (auto-sourced).
|
|
262
|
+
/// `{ ok, missing[], incompatible[] }` — names the exact directive/aggregate/field, not a hash.
|
|
263
|
+
Future<Map<String, Object?>> checkCompatibility(
|
|
264
|
+
Map<String, Object?> requirement,
|
|
265
|
+
[Map<String, Object?>? offer]) async =>
|
|
266
|
+
_asMap(await _call('checkCompatibility', {
|
|
267
|
+
'requirement': requirement,
|
|
268
|
+
if (offer != null) 'offer': offer,
|
|
269
|
+
}));
|
|
270
|
+
|
|
271
|
+
/// SHAPE COMPATIBILITY as a LIVE BINDING (#72): a stream of verdicts that re-emits whenever the
|
|
272
|
+
/// workspace's installed law changes (another peer installs/upgrades/retires a domain and it
|
|
273
|
+
/// syncs in). The verdict carries `offerKnown`. The app degrades/prompts on `ok:false`.
|
|
274
|
+
Stream<Map<String, Object?>> watchCompatibility(
|
|
275
|
+
Map<String, Object?> requirement) {
|
|
276
|
+
final subId = 'c${++_seq}';
|
|
277
|
+
late final StreamController<Map<String, Object?>> ctrl;
|
|
278
|
+
ctrl = StreamController<Map<String, Object?>>(
|
|
279
|
+
onListen: () => _call('watch', {
|
|
280
|
+
'subId': subId,
|
|
281
|
+
'kind': 'compat',
|
|
282
|
+
'requirement': requirement,
|
|
283
|
+
}),
|
|
284
|
+
onCancel: () async {
|
|
285
|
+
_compatWatches.remove(subId);
|
|
286
|
+
try {
|
|
287
|
+
await _call('unwatch', {'subId': subId});
|
|
288
|
+
} catch (_) {}
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
_compatWatches[subId] = ctrl;
|
|
292
|
+
return ctrl.stream;
|
|
293
|
+
}
|
|
294
|
+
|
|
264
295
|
Stream<List<dynamic>> _watchStream(
|
|
265
296
|
{required String kind,
|
|
266
297
|
String? queryId,
|
|
@@ -298,6 +329,10 @@ class NomosBridge {
|
|
|
298
329
|
if (!w.isClosed) await w.close();
|
|
299
330
|
}
|
|
300
331
|
_watches.clear();
|
|
332
|
+
for (final w in _compatWatches.values) {
|
|
333
|
+
if (!w.isClosed) await w.close();
|
|
334
|
+
}
|
|
335
|
+
_compatWatches.clear();
|
|
301
336
|
if (!_syncEvents.isClosed) await _syncEvents.close();
|
|
302
337
|
if (!_signalEvents.isClosed) await _signalEvents.close();
|
|
303
338
|
if (!_flameEvents.isClosed) await _flameEvents.close();
|
|
@@ -40,9 +40,11 @@ class FakeRunner implements BridgeTransport {
|
|
|
40
40
|
case 'connect':
|
|
41
41
|
_incoming.add(reply({'workspace': args['workspace']}));
|
|
42
42
|
break;
|
|
43
|
-
case '
|
|
43
|
+
case 'offer':
|
|
44
44
|
// a directive the law refuses answers ok:false (the runner frames the gate's verdict).
|
|
45
|
-
|
|
45
|
+
final intentJson =
|
|
46
|
+
utf8.decode(base64Decode(args['intentBytes'] as String));
|
|
47
|
+
if (intentJson.contains('"directiveId":"refuseMe"')) {
|
|
46
48
|
_incoming.add({
|
|
47
49
|
'v': kWebviewBridgeVersion,
|
|
48
50
|
'id': id,
|
|
@@ -50,12 +52,9 @@ class FakeRunner implements BridgeTransport {
|
|
|
50
52
|
'error': 'the law refused this intent'
|
|
51
53
|
});
|
|
52
54
|
} else {
|
|
53
|
-
_incoming.add(reply({'head': 'HEAD
|
|
55
|
+
_incoming.add(reply({'head': 'HEAD-offer'}));
|
|
54
56
|
}
|
|
55
57
|
break;
|
|
56
|
-
case 'deliverShare':
|
|
57
|
-
_incoming.add(reply({'head': 'HEAD-share'}));
|
|
58
|
-
break;
|
|
59
58
|
case 'query':
|
|
60
59
|
_incoming.add(reply({
|
|
61
60
|
'rows': [
|
|
@@ -176,16 +175,8 @@ Future<Object?> _invoke(
|
|
|
176
175
|
workspace: a['workspace'] as String,
|
|
177
176
|
clientId: a['clientId'] as String?);
|
|
178
177
|
return null;
|
|
179
|
-
case '
|
|
180
|
-
return b.
|
|
181
|
-
domain: a['domain'] as String,
|
|
182
|
-
directiveId: a['directiveId'] as String,
|
|
183
|
-
payload: (a['payload'] as Map).cast<String, Object?>(),
|
|
184
|
-
domainHash: a['domainHash'] as String);
|
|
185
|
-
case 'deliverShare':
|
|
186
|
-
return b.deliverShare(
|
|
187
|
-
quote: (a['quote'] as Map).cast<String, Object?>(),
|
|
188
|
-
domainHash: a['domainHash'] as String);
|
|
178
|
+
case 'offer':
|
|
179
|
+
return b.offer(intentBytes: a['intentBytes'] as String);
|
|
189
180
|
case 'query':
|
|
190
181
|
return b.query(
|
|
191
182
|
a['queryId'] as String, (a['params'] as Map).cast<String, Object?>());
|
|
@@ -207,8 +198,9 @@ Future<Object?> _invoke(
|
|
|
207
198
|
/// What the corpus says the typed (unwrapped) return value should be, given the reply's `result`.
|
|
208
199
|
Object? _expectedTyped(String op, Map<String, Object?> result) {
|
|
209
200
|
switch (op) {
|
|
210
|
-
case '
|
|
211
|
-
|
|
201
|
+
case 'offer':
|
|
202
|
+
return result['head'];
|
|
203
|
+
case 'offer':
|
|
212
204
|
return result['head'];
|
|
213
205
|
case 'query':
|
|
214
206
|
case 'queryById':
|
|
@@ -267,28 +259,29 @@ void main() {
|
|
|
267
259
|
await bridge.ready.timeout(const Duration(seconds: 1));
|
|
268
260
|
});
|
|
269
261
|
|
|
270
|
-
test('
|
|
262
|
+
test('offer round-trips a typed head; args reach the runner verbatim',
|
|
271
263
|
() async {
|
|
272
264
|
await bridge.ready;
|
|
273
|
-
final
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
265
|
+
final intentBytes = base64Encode(utf8.encode(jsonEncode({
|
|
266
|
+
'v': 2,
|
|
267
|
+
'intent': {
|
|
268
|
+
'domain': 'guestbook',
|
|
269
|
+
'directiveId': 'signGuestbook',
|
|
270
|
+
'payload': {'author': 'ada'},
|
|
271
|
+
'domainHash': 'abc'
|
|
272
|
+
}
|
|
273
|
+
})));
|
|
274
|
+
final head = await bridge.offer(intentBytes: intentBytes);
|
|
275
|
+
expect(head, 'HEAD-offer');
|
|
276
|
+
final req = runner.sent.firstWhere((m) => m['op'] == 'offer');
|
|
277
|
+
expect((req['args'] as Map)['intentBytes'], intentBytes);
|
|
282
278
|
});
|
|
283
279
|
|
|
284
280
|
test(
|
|
285
|
-
'connect /
|
|
281
|
+
'connect / query / queryById / count / sync / lawStatus all wrap the runner',
|
|
286
282
|
() async {
|
|
287
283
|
await bridge.ready;
|
|
288
284
|
await bridge.connect(cloud: 'https://c', workspace: 'ws', clientId: 'c1');
|
|
289
|
-
expect(
|
|
290
|
-
await bridge.deliverShare(quote: {'publicKey': 'k'}, domainHash: 'h'),
|
|
291
|
-
'HEAD-share');
|
|
292
285
|
expect((await bridge.query('byAuthor', {'author': 'ada'})).first['data'],
|
|
293
286
|
{'q': 'byAuthor'});
|
|
294
287
|
expect((await bridge.queryById('Entry_1')).first['id'], 'Entry_1');
|
|
@@ -356,8 +349,16 @@ void main() {
|
|
|
356
349
|
await bridge.ready;
|
|
357
350
|
Object? err;
|
|
358
351
|
try {
|
|
359
|
-
await bridge.
|
|
360
|
-
|
|
352
|
+
await bridge.offer(
|
|
353
|
+
intentBytes: base64Encode(utf8.encode(jsonEncode({
|
|
354
|
+
'v': 2,
|
|
355
|
+
'intent': {
|
|
356
|
+
'domain': 'x',
|
|
357
|
+
'directiveId': 'refuseMe',
|
|
358
|
+
'payload': {},
|
|
359
|
+
'domainHash': 'h'
|
|
360
|
+
}
|
|
361
|
+
}))));
|
|
361
362
|
} on NomosBridgeError catch (e) {
|
|
362
363
|
err = e;
|
|
363
364
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.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",
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
},
|
|
12
12
|
"main": "src/index.ts",
|
|
13
13
|
"bin": {
|
|
14
|
-
"nomos-compile": "
|
|
15
|
-
"holon-compile": "
|
|
14
|
+
"nomos-compile": "compile_package.mjs",
|
|
15
|
+
"holon-compile": "compile_package.mjs"
|
|
16
16
|
},
|
|
17
17
|
"exports": {
|
|
18
18
|
".": "./src/index.ts",
|
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
"src",
|
|
45
45
|
"!src/**/*.test.ts",
|
|
46
46
|
"dart",
|
|
47
|
+
"!dart/.dart_tool",
|
|
48
|
+
"!dart/build",
|
|
47
49
|
"compile_package.mjs"
|
|
48
50
|
],
|
|
49
51
|
"publishConfig": {
|
package/src/aggregate.ts
CHANGED
|
@@ -144,6 +144,13 @@ export interface AggregateHandle<
|
|
|
144
144
|
readonly hasInvariant?: true;
|
|
145
145
|
/** The executable body — ships in the engine bundle, NOT the manifest. */
|
|
146
146
|
readonly invariant?: AggregateInvariantFn;
|
|
147
|
+
/**
|
|
148
|
+
* THE DECLASSIFY WAIVER (nomosFlowGate rule 4): present iff the author acknowledged that this
|
|
149
|
+
* private aggregate's invariant refusal MAY correlate with a plaintext secret field. Rides the
|
|
150
|
+
* `nomosFlowGate` era key (replay-verified audit surface). Absent on a private+invariant
|
|
151
|
+
* aggregate ⇒ the flow gate refuses to compile (a secret-correlated refusal).
|
|
152
|
+
*/
|
|
153
|
+
readonly invariantDeclassify?: string;
|
|
147
154
|
/**
|
|
148
155
|
* The LAW-DECLARED instance CAP (Jack's ruling: instance limits are DOMAIN POLICY,
|
|
149
156
|
* never a fabric id-shape special case). `cap: 1` = at most one instance per
|
|
@@ -197,7 +204,7 @@ export interface AudienceMethods<Id extends string, F extends Record<string, Fie
|
|
|
197
204
|
export function aggregate<const Id extends string, F extends Record<string, Field>>(
|
|
198
205
|
id: Id,
|
|
199
206
|
fields: F,
|
|
200
|
-
opts?: { invariant?: AggregateInvariantFn; cap?: number; visibility?: ReadVisibility },
|
|
207
|
+
opts?: { invariant?: AggregateInvariantFn; invariantDeclassify?: string; cap?: number; visibility?: ReadVisibility },
|
|
201
208
|
): AggregateHandle<Id, F> & AudienceMethods<Id, F> {
|
|
202
209
|
const { stored, hasMany } = partitionFields(fields);
|
|
203
210
|
if (opts?.cap !== undefined && (!Number.isInteger(opts.cap) || opts.cap < 1)) {
|
|
@@ -244,6 +251,7 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
|
|
|
244
251
|
...(opts?.invariant !== undefined
|
|
245
252
|
? { hasInvariant: true as const, invariant: opts.invariant }
|
|
246
253
|
: {}),
|
|
254
|
+
...(opts?.invariantDeclassify !== undefined ? { invariantDeclassify: opts.invariantDeclassify } : {}),
|
|
247
255
|
...(opts?.cap !== undefined ? { cap: opts.cap } : {}),
|
|
248
256
|
...(encryptedFields.length > 0 ? { encryptedFields } : {}),
|
|
249
257
|
...(metaFields.length > 0 ? { metaFields } : {}),
|
package/src/authoring.ts
CHANGED
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
import type { AggregateHandle } from "./aggregate.js";
|
|
27
27
|
import type { Field } from "./fields.js";
|
|
28
28
|
import type { FieldOp, PlannedOp, PlannedEventMarker } from "./ops.js";
|
|
29
|
+
import { endorsementOf } from "./principal_evidence.js";
|
|
29
30
|
|
|
30
31
|
// ── the per-dispatch authoring sink — library state the engine never sees (NOT a side channel out of
|
|
31
32
|
// the sandbox; it is internal to the directive runtime, drained synchronously by `executeDirectiveToIntent`). ──
|
|
@@ -76,7 +77,15 @@ class Ref implements AggregateRef {
|
|
|
76
77
|
}
|
|
77
78
|
/** Emit a field op TARGETING THIS aggregate, stamped with this ref's marker (if any). */
|
|
78
79
|
private emit(field: string, kind: FieldOp["kind"], extra: Partial<FieldOp>): void {
|
|
79
|
-
|
|
80
|
+
// MATERIALISE PrincipalEvidence on the fluent surface too (the endorse model — security_model.md):
|
|
81
|
+
// `.set("owner", ctx.principal().asAttribution())` stores the RAW principal + records the endorsement
|
|
82
|
+
// so the discovery harvest + runtime witness see the flow, exactly as the typed `set()` does. A plain
|
|
83
|
+
// value passes through untouched.
|
|
84
|
+
const endorsement = "value" in extra ? endorsementOf(extra.value) : null;
|
|
85
|
+
const resolved: Partial<FieldOp> = endorsement
|
|
86
|
+
? { ...extra, value: endorsement.principal, endorsement: { facet: endorsement.facet, reason: endorsement.reason } }
|
|
87
|
+
: extra;
|
|
88
|
+
const op: FieldOp = { aggregateId: this.__id, aggregateType: this.__type, field, kind, ...resolved };
|
|
80
89
|
sink.push(this.marker ? { ...op, marker: this.marker } : op);
|
|
81
90
|
}
|
|
82
91
|
set(field: string, value: unknown): this {
|
package/src/build_package.ts
CHANGED
|
@@ -157,7 +157,7 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
157
157
|
const aggregates = [...byId.values()];
|
|
158
158
|
const directiveIds = new Set(directives.map((d) => d.id));
|
|
159
159
|
for (const task of spec.impureCapabilities ?? []) {
|
|
160
|
-
for (const directive of [task.
|
|
160
|
+
for (const directive of [task.obligation, task.complete, task.fail, task.block, task.deadLetter]) {
|
|
161
161
|
if (!directiveIds.has(directive.id)) {
|
|
162
162
|
throw new Error(
|
|
163
163
|
`domain '${spec.domain ?? spec.name}' declares impureCapability '${task.aggregate.id}' ` +
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* properties — invisible to those walks. This helper, applied at each plane's
|
|
17
17
|
* merge point (the engine lump's `mergeModules`, the package composer's merge,
|
|
18
18
|
* the compile CLI's merge), ADDS the bundle's pieces to the exports bag under
|
|
19
|
-
* synthetic names (`<export>$aggregate`, `<export>$
|
|
19
|
+
* synthetic names (`<export>$aggregate`, `<export>$obligation`, …) so every
|
|
20
20
|
* existing shape-walk discovers them with ZERO new walk semantics. The
|
|
21
21
|
* original bundle export stays (config-named lanes still resolve it); the
|
|
22
22
|
* walks ignore its unmatched shape exactly as before.
|
|
@@ -34,7 +34,7 @@ export function isCapabilityDecl(v: unknown): v is Record<string, unknown> {
|
|
|
34
34
|
);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const PIECES = ["aggregate", "
|
|
37
|
+
const PIECES = ["aggregate", "obligation", "complete", "fail", "block", "deadLetter", "tasks"] as const;
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
40
|
* Expand every `capability()` bundle in an exports bag into discoverable
|