@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.
Files changed (47) hide show
  1. package/dart/CHANGELOG.md +2 -0
  2. package/dart/FINDINGS.md +2 -2
  3. package/dart/LICENSE +45 -0
  4. package/dart/README.md +9 -0
  5. package/dart/example/nomos_webview_transport.dart +4 -4
  6. package/dart/lib/src/offer.dart +2 -2
  7. package/dart/lib/src/subscriptions.dart +1 -1
  8. package/dart/lib/src/webview_bridge.dart +52 -0
  9. package/dart/pubspec.yaml +3 -3
  10. package/dart/test/schema_validation_test.dart +1 -1
  11. package/dart/test/webview_bridge_test.dart +1 -1
  12. package/dart/test/wire_test.dart +1 -1
  13. package/package.json +1 -1
  14. package/src/aggregate.ts +9 -1
  15. package/src/authoring.ts +10 -1
  16. package/src/codegen_dart.ts +25 -15
  17. package/src/codegen_dart_client.ts +54 -5
  18. package/src/codegen_openapi.ts +239 -0
  19. package/src/codegen_proof.ts +16 -9
  20. package/src/codegen_ts.ts +83 -22
  21. package/src/compile_package_main.ts +117 -29
  22. package/src/ctx.ts +13 -0
  23. package/src/directive.ts +1 -1
  24. package/src/engine_entry.ts +22 -5
  25. package/src/fields.ts +44 -0
  26. package/src/flow_gate.ts +208 -0
  27. package/src/framework/capability.ts +19 -1
  28. package/src/framework/encryption_scope.ts +9 -1
  29. package/src/framework/genesis.ts +5 -1
  30. package/src/framework/relations.ts +23 -4
  31. package/src/framework/repair.ts +2 -2
  32. package/src/framework/signing_identity.ts +1 -1
  33. package/src/framework/workspace_invariant.ts +29 -8
  34. package/src/framework/workspaces.ts +147 -15
  35. package/src/index.ts +24 -0
  36. package/src/manifest.ts +12 -18
  37. package/src/ops.ts +42 -1
  38. package/src/principal_evidence.ts +112 -0
  39. package/src/relation_sugar.ts +10 -4
  40. package/src/shape_compat.ts +208 -0
  41. package/src/usd.ts +131 -0
  42. package/src/usd_layers.ts +43 -0
  43. package/src/wire.ts +8 -0
  44. package/src/wire_encode.ts +75 -2
  45. package/src/workspace_sharding.ts +196 -18
  46. package/src/workspace_type.ts +3 -2
  47. /package/dart/lib/{nomos_dsl.dart → nomos_client.dart} +0 -0
@@ -0,0 +1,2 @@
1
+ ## 0.16.0
2
+ - First pub.dev release: kernel wire types, `NomosBridge` protocol, typed offer/query/watch surface.
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 (`nomos_dsl`):
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/nomos_dsl.dart` | Public barrel (exports the three above). | hand-written |
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 `nomos_dsl` library: it depends on
2
- // `webview_flutter`, which the pure-Dart `nomos_dsl` package deliberately does NOT depend on (so the bridge
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:nomos_dsl/nomos_dsl.dart`) over a `webview_flutter`
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:nomos_dsl/nomos_dsl.dart';
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.
@@ -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 `nomos_dsl` package stays free of a `flutter_rust_bridge` dependency
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 `nomos_dsl` package stays free of a
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 `nomos_dsl` package stays host-agnostic. The app threads `client.query`
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: nomos_dsl
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.0.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:nomos_dsl/nomos_dsl.dart';
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:nomos_dsl/nomos_dsl.dart';
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
@@ -7,7 +7,7 @@
7
7
 
8
8
  import 'dart:convert';
9
9
 
10
- import 'package:nomos_dsl/nomos_dsl.dart';
10
+ import 'package:nomos_client/nomos_client.dart';
11
11
  import 'package:test/test.dart';
12
12
 
13
13
  /// Deep round-trip: a JSON string -> Dart fromJson -> toJson -> re-encoded
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.15.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
- const op: FieldOp = { aggregateId: this.__id, aggregateType: this.__type, field, kind, ...extra };
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 {
@@ -671,13 +671,24 @@ function aggregateFieldSpecs(
671
671
  f.kind === "string" && ownIdFields.has(name)
672
672
  ? aggregateIdDartType(agg.id)
673
673
  : undefined;
674
- const spec = aggFieldSpec(
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. Collection fields (\`set\`/\`map\`) default to`,
1053
- `/// EMPTY and \`.optional()\` scalars to \`null\` when a freshly-folded aggregate has`,
1054
- `/// not yet folded them; must-be-folded scalars still throw when absent.`,
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). Missing REQUIRED fields throw a`,
1066
- ` /// [FormatException]; absent collections decode empty and absent \`.optional()\``,
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
- `/// Collection fields (\`set\`/\`map\`) default to EMPTY and \`.optional()\` scalars to`,
1475
- `/// \`null\` when a freshly-folded aggregate has not yet folded them.`,
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). Missing REQUIRED fields throw a`,
1487
- ` /// [FormatException]; absent collections decode empty and absent \`.optional()\` scalars`,
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 `nomos_dsl` package by its PUBLIC package: URI — NOT a relative
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 nomos_dsl into the co-located tenant package). The framework barrel
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:nomos_dsl/nomos_dsl.dart';`,
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
- * `nomos_dsl.dart` barrel (for `NomosBridge`). NO manifest/hash impact — pure frontend codegen, exactly
26
- * like `codegen_dart.ts`. NO FALLBACK: an unsupported shape throws at compile time.
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 'nomos_dsl.dart';`,
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 'nomos_dsl.dart' show NomosBridge, BridgeTransport, NomosBridgeError, kWebviewBridgeVersion;`,
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
  ``,