@githolon/dsl 0.15.0 → 0.16.1
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 +2 -0
- package/dart/FINDINGS.md +2 -2
- package/dart/LICENSE +45 -0
- package/dart/README.md +9 -0
- package/dart/example/nomos_webview_transport.dart +4 -4
- package/dart/lib/src/offer.dart +2 -2
- package/dart/lib/src/subscriptions.dart +1 -1
- package/dart/lib/src/webview_bridge.dart +52 -0
- package/dart/pubspec.yaml +3 -3
- package/dart/test/schema_validation_test.dart +1 -1
- package/dart/test/webview_bridge_test.dart +1 -1
- package/dart/test/wire_test.dart +1 -1
- package/package.json +1 -1
- package/src/aggregate.ts +9 -1
- package/src/authoring.ts +10 -1
- package/src/codegen_dart.ts +25 -15
- package/src/codegen_dart_client.ts +54 -5
- package/src/codegen_openapi.ts +239 -0
- package/src/codegen_proof.ts +16 -9
- package/src/codegen_ts.ts +83 -22
- package/src/compile_package_main.ts +117 -29
- package/src/ctx.ts +13 -0
- package/src/directive.ts +1 -1
- 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 +19 -1
- package/src/framework/encryption_scope.ts +9 -1
- package/src/framework/genesis.ts +5 -1
- package/src/framework/relations.ts +23 -4
- package/src/framework/repair.ts +2 -2
- package/src/framework/signing_identity.ts +1 -1
- package/src/framework/workspace_invariant.ts +29 -8
- package/src/framework/workspaces.ts +147 -15
- package/src/index.ts +24 -0
- package/src/manifest.ts +12 -18
- 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 +131 -0
- package/src/usd_layers.ts +43 -0
- package/src/wire.ts +8 -0
- package/src/wire_encode.ts +75 -2
- package/src/workspace_sharding.ts +196 -18
- package/src/workspace_type.ts +3 -2
- /package/dart/lib/{nomos_dsl.dart → nomos_client.dart} +0 -0
package/dart/FINDINGS.md
CHANGED
|
@@ -16,14 +16,14 @@ design: the schema is the single source of truth → TS (done), proto (later),
|
|
|
16
16
|
|
|
17
17
|
## What was built
|
|
18
18
|
|
|
19
|
-
`nomos2/dsl/dart/` — a Dart package (`
|
|
19
|
+
`nomos2/dsl/dart/` — a Dart package (`nomos_client`):
|
|
20
20
|
|
|
21
21
|
| Path | Role | Hand-written / generated |
|
|
22
22
|
|---|---|---|
|
|
23
23
|
| `lib/src/wire.dart` | Fixed kernel wire types: `Hlc`, `Value` (sealed: `ValueStr`/`ValueInt`/`ValueSet`/`ValueMap`), `WireField`, `Op` (sealed: `OpSet`/`OpAddToSet`/`OpSetEntry`), `FieldOp`, `Event` (`{aggregate, ops}`, #56), `Intent` (`{hlc, events}`, #56). | hand-written |
|
|
24
24
|
| `lib/src/driver.dart` | `WireDriver` (sealed: `DriverLww`/`DriverAddWins`/`DriverConflict`/`DriverMapOf`) + `Schema`. | hand-written |
|
|
25
25
|
| `lib/src/generated/trackable_asset.dart` | The `trackable_asset` domain: `TrackableAsset` aggregate, `StatusEnum`, and `CreateAssetPayload`/`MoveAssetPayload`/`TagAssetPayload`/`SetCustomFieldPayload`. | **generated** |
|
|
26
|
-
| `lib/
|
|
26
|
+
| `lib/nomos_client.dart` | Public barrel (exports the three above). | hand-written |
|
|
27
27
|
| `test/wire_test.dart` | Round-trip tests for every wire type + Driver/Schema. | — |
|
|
28
28
|
| `test/trackable_asset_test.dart` | Read-model aggregate deserialization + directive-payload → Event JSON. | — |
|
|
29
29
|
|
package/dart/LICENSE
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Nomos Pre-Release License (v1)
|
|
2
|
+
|
|
3
|
+
Copyright © 2026 Captain App Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This is a pre-release of Nomos. This license gives you what you need to BUILD
|
|
6
|
+
with it; we keep the rest for now.
|
|
7
|
+
|
|
8
|
+
## You may
|
|
9
|
+
|
|
10
|
+
- install and use these packages to author Nomos domains, compile them, and
|
|
11
|
+
deploy them to Nomos Cloud or any Nomos instance Captain App operates or
|
|
12
|
+
authorizes;
|
|
13
|
+
- build, run, and ship applications on top of them — including commercial ones;
|
|
14
|
+
- keep everything that's yours: code you write, and everything these tools
|
|
15
|
+
generate FOR you (scaffolds from `create-githolon` / `githolon generate`, generated
|
|
16
|
+
clients, compiled domain packages) carries NO restriction from us — it is
|
|
17
|
+
yours outright.
|
|
18
|
+
|
|
19
|
+
## You may not
|
|
20
|
+
|
|
21
|
+
- redistribute these packages or their source, in whole or in part, outside
|
|
22
|
+
your team;
|
|
23
|
+
- modify them or build derivative tools, SDKs, or runtimes from their source;
|
|
24
|
+
- offer the Nomos runtime, or anything materially similar, as a hosted service;
|
|
25
|
+
- reverse-engineer the holon wasm runtime.
|
|
26
|
+
|
|
27
|
+
## Data retention
|
|
28
|
+
|
|
29
|
+
This is a pre-release we are actively evaluating. Workspaces you retire stop
|
|
30
|
+
counting toward your quota but are NOT deleted: **we retain all workspace data
|
|
31
|
+
(ledgers, law, intents) and may examine it to evaluate how the product is
|
|
32
|
+
used.** Don't put anything in a pre-release workspace you wouldn't want the
|
|
33
|
+
builders to read. If you need something truly expunged, ask:
|
|
34
|
+
jack@captainapp.co.uk.
|
|
35
|
+
|
|
36
|
+
## The rest
|
|
37
|
+
|
|
38
|
+
Provided **as is**, with no warranty of any kind; to the maximum extent
|
|
39
|
+
permitted by law, Captain App Ltd accepts no liability arising from your use.
|
|
40
|
+
This license terminates automatically if you breach it.
|
|
41
|
+
|
|
42
|
+
Want the kernel source, broader rights, or to do something this doesn't cover?
|
|
43
|
+
**Ask: jack@captainapp.co.uk.** The plan is to open Nomos up gradually, with
|
|
44
|
+
the people actually using it — telling us what you're building is how that
|
|
45
|
+
happens faster.
|
package/dart/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# nomos_client
|
|
2
|
+
|
|
3
|
+
Dart frontend types for [Nomos](https://github.com/Captain-App/nomos2) — the hand-written kernel
|
|
4
|
+
wire types plus the code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
5
|
+
the exact serde JSON the Rust kernel + projection produce/consume, and defines the `NomosBridge`
|
|
6
|
+
protocol the runtime ([`nomos_flutter`](https://pub.dev/packages/nomos_flutter)) drives.
|
|
7
|
+
|
|
8
|
+
You normally depend on `nomos_flutter` (which re-exports the bridge surface) rather than this package
|
|
9
|
+
directly. Licensed under the Nomos Pre-Release License — see `LICENSE`.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// COPY THIS INTO YOUR FLUTTER APP. It is an EXAMPLE, not part of the `
|
|
2
|
-
// `webview_flutter`, which the pure-Dart `
|
|
1
|
+
// COPY THIS INTO YOUR FLUTTER APP. It is an EXAMPLE, not part of the `nomos_client` library: it depends on
|
|
2
|
+
// `webview_flutter`, which the pure-Dart `nomos_client` package deliberately does NOT depend on (so the bridge
|
|
3
3
|
// protocol logic stays Flutter-free and unit-testable). This ~50-line transport is the only app-specific glue.
|
|
4
4
|
//
|
|
5
|
-
// It implements `BridgeTransport` (from `package:
|
|
5
|
+
// It implements `BridgeTransport` (from `package:nomos_client/nomos_client.dart`) over a `webview_flutter`
|
|
6
6
|
// WebViewController loading the published `nomos-runner` bundle:
|
|
7
7
|
// • host → runner: `controller.runJavaScript("window.__nomosSend(<json>)")`
|
|
8
8
|
// • runner → host: a `JavaScriptChannel("NomosHost")` whose onMessage feeds the bridge's message stream.
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import 'dart:async';
|
|
25
25
|
import 'dart:convert';
|
|
26
26
|
|
|
27
|
-
import 'package:
|
|
27
|
+
import 'package:nomos_client/nomos_client.dart';
|
|
28
28
|
import 'package:webview_flutter/webview_flutter.dart';
|
|
29
29
|
|
|
30
30
|
/// A [BridgeTransport] over a `webview_flutter` controller hosting the `nomos-runner` bundle.
|
package/dart/lib/src/offer.dart
CHANGED
|
@@ -40,7 +40,7 @@ abstract interface class NomosIntent {
|
|
|
40
40
|
|
|
41
41
|
/// The kernel write seam: the FRB `NomosClient.author` signature, as a function
|
|
42
42
|
/// type. `offer` takes this (rather than importing the FRB client) so the
|
|
43
|
-
/// generated `
|
|
43
|
+
/// generated `nomos_client` 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({
|
|
46
46
|
required String domain,
|
|
@@ -70,7 +70,7 @@ String offer(NomosIntent intent,
|
|
|
70
70
|
// this seam to RESERVE a typed, type-tagged `<Type>_<uuidv7>` id, which it ships in the
|
|
71
71
|
// create payload; the kernel's GATE verifies its shape + type-tag prefix on author. Like
|
|
72
72
|
// [Authorer], this is a function type the app threads in (the FRB
|
|
73
|
-
// `NomosClient.mintIdForWorkspace`), so the generated `
|
|
73
|
+
// `NomosClient.mintIdForWorkspace`), so the generated `nomos_client` package stays free of a
|
|
74
74
|
// `flutter_rust_bridge` dependency.
|
|
75
75
|
//
|
|
76
76
|
// WHERE THE RANDOMNESS LIVES (the model — the dev touches NONE of it): the KERNEL mints
|
|
@@ -38,7 +38,7 @@ import 'dart:convert';
|
|
|
38
38
|
/// data}` rows (O(matches), never a workspace scan — #140 READ-CLOSURE).
|
|
39
39
|
///
|
|
40
40
|
/// `subscriptions.dart` takes this (rather than importing a host client) so the
|
|
41
|
-
/// generated `
|
|
41
|
+
/// generated `nomos_client` package stays host-agnostic. The app threads `client.query`
|
|
42
42
|
/// in. `paramsJson` is a JSON object carrying the
|
|
43
43
|
/// key field + value, e.g. `{"estateId":"e-1"}`.
|
|
44
44
|
typedef Queryer = String Function(String queryId, String paramsJson);
|
|
@@ -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
|
}
|
|
@@ -244,6 +257,41 @@ class NomosBridge {
|
|
|
244
257
|
Stream<List<dynamic>> watchById(String id) =>
|
|
245
258
|
_watchStream(kind: 'byId', id: id);
|
|
246
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
|
+
|
|
247
295
|
Stream<List<dynamic>> _watchStream(
|
|
248
296
|
{required String kind,
|
|
249
297
|
String? queryId,
|
|
@@ -281,6 +329,10 @@ class NomosBridge {
|
|
|
281
329
|
if (!w.isClosed) await w.close();
|
|
282
330
|
}
|
|
283
331
|
_watches.clear();
|
|
332
|
+
for (final w in _compatWatches.values) {
|
|
333
|
+
if (!w.isClosed) await w.close();
|
|
334
|
+
}
|
|
335
|
+
_compatWatches.clear();
|
|
284
336
|
if (!_syncEvents.isClosed) await _syncEvents.close();
|
|
285
337
|
if (!_signalEvents.isClosed) await _signalEvents.close();
|
|
286
338
|
if (!_flameEvents.isClosed) await _flameEvents.close();
|
package/dart/pubspec.yaml
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
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
|
-
name:
|
|
8
|
+
name: nomos_client
|
|
9
|
+
repository: https://github.com/Captain-App/nomos2
|
|
9
10
|
description: >-
|
|
10
11
|
Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
|
|
11
12
|
code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
12
13
|
the EXACT serde JSON the Rust kernel + projection produce/consume.
|
|
13
|
-
version: 0.
|
|
14
|
-
publish_to: none
|
|
14
|
+
version: 0.16.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
|
@@ -5,7 +5,7 @@
|
|
|
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
|
-
import 'package:
|
|
8
|
+
import 'package:nomos_client/nomos_client.dart';
|
|
9
9
|
import 'package:test/test.dart';
|
|
10
10
|
|
|
11
11
|
void main() {
|
|
@@ -7,7 +7,7 @@ import 'dart:async';
|
|
|
7
7
|
import 'dart:convert';
|
|
8
8
|
import 'dart:io';
|
|
9
9
|
|
|
10
|
-
import 'package:
|
|
10
|
+
import 'package:nomos_client/nomos_client.dart';
|
|
11
11
|
import 'package:test/test.dart';
|
|
12
12
|
|
|
13
13
|
/// A fake runner-over-a-transport: records sent requests, replies the way `webview-runner.mjs` does, and can
|
package/dart/test/wire_test.dart
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
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",
|
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/codegen_dart.ts
CHANGED
|
@@ -671,13 +671,24 @@ function aggregateFieldSpecs(
|
|
|
671
671
|
f.kind === "string" && ownIdFields.has(name)
|
|
672
672
|
? aggregateIdDartType(agg.id)
|
|
673
673
|
: undefined;
|
|
674
|
-
const
|
|
674
|
+
const base = aggFieldSpec(
|
|
675
675
|
name,
|
|
676
676
|
f,
|
|
677
677
|
enumDartTypes,
|
|
678
678
|
refIdClassForAggregate,
|
|
679
679
|
valueClass,
|
|
680
680
|
);
|
|
681
|
+
// PARTIAL-FOLD DOCTRINE (every read-model field is nullable). The projection
|
|
682
|
+
// serves PARTIAL folds: a freshly-created or not-yet-fully-folded aggregate may
|
|
683
|
+
// carry no value for ANY scalar (only the fields touched so far appear in the
|
|
684
|
+
// flat `data`). So a read-model SCALAR is ALWAYS nullable — never a strict
|
|
685
|
+
// `as bool`/`readInt` that throws on an absent field (the `watch`/`read`
|
|
686
|
+
// decode would crash mid-fold). `.optional()` on the DSL field no longer
|
|
687
|
+
// changes the read surface; it was nullable in practice — this makes the type
|
|
688
|
+
// honest. Collections are unchanged (they already empty-default when absent).
|
|
689
|
+
const spec: AggFieldSpec = isCollectionKind(base.kind)
|
|
690
|
+
? base
|
|
691
|
+
: { ...base, optional: true };
|
|
681
692
|
if (used.has(spec.name)) {
|
|
682
693
|
throw new Error(
|
|
683
694
|
`aggregate '${agg.id}' field '${spec.wireName}' maps to duplicate Dart field '${spec.name}'`,
|
|
@@ -1049,9 +1060,9 @@ function emitAggregate(
|
|
|
1049
1060
|
`/// projection \`data\` map (the \`{type,id,data}\` row \`query()\`/\`query_by_id()\``,
|
|
1050
1061
|
`/// emits) — e.g. \`{"name":"HQ","createdAt":1000}\`. The kernel aggregate`,
|
|
1051
1062
|
`/// [aggregateId] is carried from the row \`id\`. Read it reactively through the`,
|
|
1052
|
-
`/// generated \`watch…\` accessors below.
|
|
1053
|
-
`///
|
|
1054
|
-
`///
|
|
1063
|
+
`/// generated \`watch…\` accessors below. The projection serves PARTIAL folds, so`,
|
|
1064
|
+
`/// EVERY scalar field is nullable — \`null\` when a freshly-folded aggregate has not`,
|
|
1065
|
+
`/// yet folded it (never a strict cast that throws); collections default to EMPTY.`,
|
|
1055
1066
|
`class ${className} {`,
|
|
1056
1067
|
fieldDecls,
|
|
1057
1068
|
``,
|
|
@@ -1062,9 +1073,8 @@ function emitAggregate(
|
|
|
1062
1073
|
` /// Parse from the FLAT projection \`data\` map (the \`{type,id,data}\` row's`,
|
|
1063
1074
|
` /// \`data\` that \`query()\`/\`query_by_id()\` emit — fields already`,
|
|
1064
1075
|
` /// decoded to bare JSON), carrying the kernel aggregate [id] from the row \`id\``,
|
|
1065
|
-
` /// (defaults to '' for a bare-map call).
|
|
1066
|
-
` ///
|
|
1067
|
-
` /// scalars decode \`null\`.`,
|
|
1076
|
+
` /// (defaults to '' for a bare-map call). PARTIAL folds: every scalar decodes`,
|
|
1077
|
+
` /// \`null\` when absent (never throws); absent collections decode empty.`,
|
|
1068
1078
|
` factory ${className}.fromJson(Map<String, dynamic> data, [String id = '']) {`,
|
|
1069
1079
|
` return ${className}(`,
|
|
1070
1080
|
parseLines,
|
|
@@ -1471,8 +1481,9 @@ function emitReadModel(
|
|
|
1471
1481
|
`/// DSL-named; a \`json\` value-object leaf (e.g. \`location\`)`,
|
|
1472
1482
|
`/// is a STRUCTURED \`Map<String, Object?>\` — the decoded shape the engine emits, NOT a`,
|
|
1473
1483
|
`/// JSON string. This class is fully DSL-derived.`,
|
|
1474
|
-
`///
|
|
1475
|
-
`/// \`null\` when a freshly-folded aggregate has not yet folded
|
|
1484
|
+
`/// The projection serves PARTIAL folds, so EVERY scalar field is nullable —`,
|
|
1485
|
+
`/// \`null\` when a freshly-folded aggregate has not yet folded it (never a strict`,
|
|
1486
|
+
`/// cast that throws); collection fields (\`set\`/\`map\`) default to EMPTY.`,
|
|
1476
1487
|
`class ${className} {`,
|
|
1477
1488
|
fieldDecls,
|
|
1478
1489
|
``,
|
|
@@ -1483,9 +1494,8 @@ function emitReadModel(
|
|
|
1483
1494
|
` /// Parse from the Rust read engine's already-decoded projected \`data\` map (the`,
|
|
1484
1495
|
` /// \`{type,id,data}\` row's \`data\` that \`query()\`/\`query_by_id()\` emit — every field`,
|
|
1485
1496
|
` /// already decoded to bare/structured JSON), carrying the kernel aggregate [id] from`,
|
|
1486
|
-
` /// the row \`id\` (defaults to '' for a bare-map call).
|
|
1487
|
-
` ///
|
|
1488
|
-
` /// decode \`null\`.`,
|
|
1497
|
+
` /// the row \`id\` (defaults to '' for a bare-map call). PARTIAL folds: every scalar`,
|
|
1498
|
+
` /// decodes \`null\` when absent (never throws); absent collections decode empty.`,
|
|
1489
1499
|
` factory ${className}.fromProjected(Map<String, dynamic> data, [String id = '']) {`,
|
|
1490
1500
|
` return ${className}(`,
|
|
1491
1501
|
parseLines,
|
|
@@ -2732,11 +2742,11 @@ export function generateDartDomain(mod: DomainModule): string {
|
|
|
2732
2742
|
`// ignore_for_file: unused_import`,
|
|
2733
2743
|
``,
|
|
2734
2744
|
// The generated tenant domains live in the `co2_nomos_domains` package and depend
|
|
2735
|
-
// on the framework `
|
|
2745
|
+
// on the framework `nomos_client` package by its PUBLIC package: URI — NOT a relative
|
|
2736
2746
|
// `../wire.dart` path into the framework (which broke when the generated dir moved
|
|
2737
|
-
// out of
|
|
2747
|
+
// out of nomos_client into the co-located tenant package). The framework barrel
|
|
2738
2748
|
// re-exports wire/dispatch/subscriptions/provider, so one import covers them all.
|
|
2739
|
-
`import 'package:
|
|
2749
|
+
`import 'package:nomos_client/nomos_client.dart';`,
|
|
2740
2750
|
...(mod.dartImports ?? []).map(dartImportLine),
|
|
2741
2751
|
``,
|
|
2742
2752
|
`// ---- enums ----`,
|
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
* * by-id read/watch + a type-index `list…` per aggregate;
|
|
22
22
|
* * typed O(1) count/sum accessors.
|
|
23
23
|
*
|
|
24
|
-
* It IMPORTS the sibling `<name>.dart` (for the read-model + payload classes) and the vendored
|
|
25
|
-
* `
|
|
26
|
-
* like `codegen_dart.ts`. NO FALLBACK: an unsupported shape throws at compile
|
|
24
|
+
* It IMPORTS the sibling `<name>.dart` (for the read-model + payload classes) and either the vendored
|
|
25
|
+
* `nomos_client.dart` barrel or the package `nomos_client` barrel (for `NomosBridge`). NO manifest/hash impact —
|
|
26
|
+
* pure frontend codegen, exactly like `codegen_dart.ts`. NO FALLBACK: an unsupported shape throws at compile
|
|
27
|
+
* time.
|
|
27
28
|
*/
|
|
28
29
|
import type { z } from "zod";
|
|
29
30
|
|
|
@@ -56,6 +57,15 @@ export const dartClientClassName = (domain: string): string => `${cap(camel(doma
|
|
|
56
57
|
/** `guestbook` → `guestbookDomainHash` — the Dart top-level const (lowerCamel by lint). */
|
|
57
58
|
export const dartHashConstName = (fileBase: string): string => `${lcFirst(camel(fileBase))}DomainHash`;
|
|
58
59
|
|
|
60
|
+
/** `todo-nomos` → `todoNomosInterfaceJson` — the baked structural-interface JSON const (#72). */
|
|
61
|
+
export const interfaceConstNameDart = (fileBase: string): string => `${lcFirst(camel(fileBase))}InterfaceJson`;
|
|
62
|
+
|
|
63
|
+
/** Emit a Dart RAW triple-quoted string literal of an object's JSON (the interface is JSON-only
|
|
64
|
+
* data — no `'`/`$`/backslash escaping concerns under `r'''…'''`; JSON never contains `'''`). */
|
|
65
|
+
function dartRawJson(value: unknown): string {
|
|
66
|
+
return `r'''${JSON.stringify(value)}'''`;
|
|
67
|
+
}
|
|
68
|
+
|
|
59
69
|
const dartStr = (s: string) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
60
70
|
|
|
61
71
|
/** The Dart param type for a query index key (int-kinded keys → `int`, else `String`). */
|
|
@@ -83,6 +93,12 @@ function rowToModel(model: string, rowVar: string): string {
|
|
|
83
93
|
export interface DartBridgeClientOptions {
|
|
84
94
|
/** sha256 of the built `.package.usda` — the law this client offers under. */
|
|
85
95
|
readonly domainHash: string;
|
|
96
|
+
/** This domain's STRUCTURAL INTERFACE (#72) — directives (payload shapes) + aggregates
|
|
97
|
+
* (read shapes, sid-keyed). Baked in as JSON so the app can negotiate shape-compatibility
|
|
98
|
+
* against the workspace's installed offer, NOT by law hash. */
|
|
99
|
+
readonly interface?: unknown;
|
|
100
|
+
/** Where the bridge/runtime support types are imported from. */
|
|
101
|
+
readonly supportImport?: "vendored" | "package";
|
|
86
102
|
}
|
|
87
103
|
|
|
88
104
|
/** Generate the `<name>.client.dart` async bridge client for one domain module. */
|
|
@@ -90,6 +106,10 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
90
106
|
const domain = mod.domain ?? mod.name;
|
|
91
107
|
const className = dartClientClassName(domain);
|
|
92
108
|
const hashConst = dartHashConstName(mod.name);
|
|
109
|
+
const supportImport =
|
|
110
|
+
opts.supportImport === "package"
|
|
111
|
+
? "package:nomos_client/nomos_client.dart"
|
|
112
|
+
: "nomos_client.dart";
|
|
93
113
|
const aggById = new Map<string, AggregateHandle>();
|
|
94
114
|
for (const a of mod.aggregates) aggById.set(a.id, a);
|
|
95
115
|
|
|
@@ -198,16 +218,26 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
198
218
|
``,
|
|
199
219
|
`import 'dart:convert';`,
|
|
200
220
|
``,
|
|
201
|
-
`import '
|
|
221
|
+
`import '${supportImport}';`,
|
|
202
222
|
`import '${mod.name}.dart';`,
|
|
203
223
|
`// Re-export the sibling types (read models + payloads + the bridge runtime) so ONE import of this`,
|
|
204
224
|
`// client file gives an app the whole typed surface.`,
|
|
205
225
|
`export '${mod.name}.dart';`,
|
|
206
|
-
`export '
|
|
226
|
+
`export '${supportImport}' show NomosBridge, BridgeTransport, NomosBridgeError, kWebviewBridgeVersion;`,
|
|
207
227
|
``,
|
|
208
228
|
`/// sha256 of the deployed \`.package.usda\` — the installed law this client offers under.`,
|
|
209
229
|
`const String ${hashConst} = ${dartStr(opts.domainHash)};`,
|
|
210
230
|
``,
|
|
231
|
+
...(opts.interface !== undefined
|
|
232
|
+
? [
|
|
233
|
+
`/// This client's STRUCTURAL INTERFACE REQUIREMENT (#72) as JSON — the directives (payload`,
|
|
234
|
+
`/// shapes) + aggregates (read shapes, sid-keyed) it depends on. \`connect()\` negotiates this`,
|
|
235
|
+
`/// against the workspace's installed offer by SHAPE (stable-id subtyping), not by law hash,`,
|
|
236
|
+
`/// so the domain can evolve without republishing this client.`,
|
|
237
|
+
`const String ${interfaceConstNameDart(mod.name)} = ${dartRawJson(opts.interface)};`,
|
|
238
|
+
``,
|
|
239
|
+
]
|
|
240
|
+
: []),
|
|
211
241
|
`String _encodeIntentOffer(String domain, String directiveId, Object? payload, String domainHash) =>`,
|
|
212
242
|
` base64Encode(utf8.encode(jsonEncode({`,
|
|
213
243
|
` 'v': 2,`,
|
|
@@ -225,6 +255,25 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
225
255
|
` final String domainHash;`,
|
|
226
256
|
` ${className}(this.bridge, {this.domainHash = ${hashConst}});`,
|
|
227
257
|
``,
|
|
258
|
+
...(opts.interface !== undefined
|
|
259
|
+
? [
|
|
260
|
+
` /// This client's structural interface requirement (#72) as JSON — hand it to the`,
|
|
261
|
+
` /// bridge's compatibility check to negotiate by SHAPE against the workspace's offer.`,
|
|
262
|
+
` String get interfaceJson => ${interfaceConstNameDart(mod.name)};`,
|
|
263
|
+
``,
|
|
264
|
+
` Map<String, Object?> get _requirement =>`,
|
|
265
|
+
` {${dartStr(domain)}: (jsonDecode(interfaceJson) as Map).cast<String, Object?>()};`,
|
|
266
|
+
``,
|
|
267
|
+
` /// SHAPE COMPATIBILITY (#72): one-shot verdict for THIS client against the workspace's`,
|
|
268
|
+
` /// ON-LEDGER offer — \`{ ok, missing[], incompatible[] }\`, by shape not law hash.`,
|
|
269
|
+
` Future<Map<String, Object?>> checkCompatibility() => bridge.checkCompatibility(_requirement);`,
|
|
270
|
+
``,
|
|
271
|
+
` /// SHAPE COMPATIBILITY as a LIVE BINDING (#72): a stream of verdicts that re-emits whenever`,
|
|
272
|
+
` /// the workspace's installed law changes (a synced install/upgrade from another peer).`,
|
|
273
|
+
` Stream<Map<String, Object?>> watchCompatibility() => bridge.watchCompatibility(_requirement);`,
|
|
274
|
+
``,
|
|
275
|
+
]
|
|
276
|
+
: []),
|
|
228
277
|
...methods,
|
|
229
278
|
`}`,
|
|
230
279
|
``,
|