@githolon/dsl 0.15.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/lib/src/webview_bridge.dart +52 -0
- package/package.json +1 -1
- package/src/aggregate.ts +9 -1
- package/src/authoring.ts +10 -1
- package/src/codegen_dart.ts +22 -12
- package/src/codegen_dart_client.ts +54 -5
- package/src/codegen_ts.ts +32 -7
- package/src/compile_package_main.ts +90 -20
- 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 +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/signing_identity.ts +1 -1
- package/src/framework/workspace_invariant.ts +29 -8
- package/src/framework/workspaces.ts +7 -0
- package/src/index.ts +24 -0
- 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 +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 +195 -17
- package/src/workspace_type.ts +1 -0
|
@@ -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/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",
|
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,
|
|
@@ -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 —
|
|
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_dsl.dart` barrel or the package `nomos_dsl` 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_dsl/nomos_dsl.dart"
|
|
112
|
+
: "nomos_dsl.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
|
``,
|
package/src/codegen_ts.ts
CHANGED
|
@@ -64,6 +64,9 @@ export const tsClientFactoryName = (domain: string): string => `${lcFirst(camel(
|
|
|
64
64
|
/** `tool-library` / `tool_library` / `Tool Library` → `TOOL_LIBRARY_DOMAIN_HASH` — the hash const. */
|
|
65
65
|
export const tsHashConstName = (packageName: string): string => `${upperSnake(packageName)}_DOMAIN_HASH`;
|
|
66
66
|
|
|
67
|
+
/** `tool-library` → `TOOL_LIBRARY_INTERFACE` — the baked structural-interface const (#72). */
|
|
68
|
+
export const interfaceConstName = (packageName: string): string => `${upperSnake(packageName)}_INTERFACE`;
|
|
69
|
+
|
|
67
70
|
// ── zod → TS (the same `_def ?? def` convention as codegen_dart's walker) ─────────
|
|
68
71
|
// The walkers are exported: codegen_proof.ts synthesizes SAMPLE payloads from the
|
|
69
72
|
// same zod shapes this file derives TYPES from — one introspection, two emitters.
|
|
@@ -289,6 +292,10 @@ export interface TsClientOptions {
|
|
|
289
292
|
readonly packageName: string;
|
|
290
293
|
/** sha256 of the built `.package.usda` — the law this client offers under. */
|
|
291
294
|
readonly domainHash: string;
|
|
295
|
+
/** The package's STRUCTURAL INTERFACE (#72) — directives (payload shapes) + aggregates
|
|
296
|
+
* (read shapes, sid-keyed), keyed by domain. Baked in so `connect()` can negotiate
|
|
297
|
+
* shape-compatibility against the workspace's installed offer — NOT by hash. */
|
|
298
|
+
readonly interface?: Record<string, unknown>;
|
|
292
299
|
}
|
|
293
300
|
|
|
294
301
|
/** TRUE when any module declares a PACKED workspace taxonomy (the routed-holon shape).
|
|
@@ -322,6 +329,16 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
322
329
|
`/** sha256 of the deployed \`.package.usda\` — the installed law this client offers under. */`,
|
|
323
330
|
`export const ${hashConst} = ${JSON.stringify(opts.domainHash)};`,
|
|
324
331
|
``,
|
|
332
|
+
...(opts.interface !== undefined
|
|
333
|
+
? [
|
|
334
|
+
`/** This client's STRUCTURAL INTERFACE REQUIREMENT (#72): the directives (payload shapes)`,
|
|
335
|
+
` * and aggregates (read shapes, sid-keyed) it depends on, keyed by domain. \`connect()\``,
|
|
336
|
+
` * negotiates this against the workspace's installed offer by SHAPE (stable-id subtyping),`,
|
|
337
|
+
` * not by law hash — so the domain can evolve without republishing this client. */`,
|
|
338
|
+
`export const ${interfaceConstName(opts.packageName)} = ${JSON.stringify(opts.interface)} as const;`,
|
|
339
|
+
``,
|
|
340
|
+
]
|
|
341
|
+
: []),
|
|
325
342
|
`/** One projection row: the aggregate's wire type, its id, and the folded fields. */`,
|
|
326
343
|
`export interface Row<T> { type: string; id: string; data: T }`,
|
|
327
344
|
``,
|
|
@@ -347,8 +364,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
347
364
|
` discardDeadLetter(id: string): Promise<{ ok: boolean; removed: number }>;`,
|
|
348
365
|
`}`,
|
|
349
366
|
``,
|
|
350
|
-
`const encodeIntentOffer = (domain: string, directiveId: string, payload: unknown, domainHash: string): Uint8Array =>`,
|
|
351
|
-
` new TextEncoder().encode(JSON.stringify({ v: 2, intent: { domain, directiveId, payload, domainHash } }));`,
|
|
367
|
+
`const encodeIntentOffer = (domain: string, directiveId: string, payload: unknown, domainHash: string, actor?: string): Uint8Array =>`,
|
|
368
|
+
` new TextEncoder().encode(JSON.stringify({ v: 2, intent: { domain, directiveId, payload, domainHash }, ...(actor ? { security: { actor } } : {}) }));`,
|
|
352
369
|
``,
|
|
353
370
|
);
|
|
354
371
|
|
|
@@ -595,7 +612,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
595
612
|
` * — ids stay internal plumbing — and returns it with the new local head. */`,
|
|
596
613
|
` ${d.id}: async (payload: ${payloadName}): Promise<{ head: string; ${mint.field}: string }> => {`,
|
|
597
614
|
` const ${mint.field} = payload.${mint.field} ?? (await (holon as NomosRoutedHolon).mint(${JSON.stringify(mint.mintType)}${mintOpts}));`,
|
|
598
|
-
` const head = await holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, domainHash));`,
|
|
615
|
+
` const head = await holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, domainHash, actor));`,
|
|
599
616
|
` return { head, ${mint.field} };`,
|
|
600
617
|
` },`,
|
|
601
618
|
);
|
|
@@ -604,8 +621,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
604
621
|
methods.push(
|
|
605
622
|
` /** Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head. */`,
|
|
606
623
|
stamps.length
|
|
607
|
-
? ` ${d.id}: (payload: ${payloadName}) => holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, stampNow(payload, ${JSON.stringify(stamps)}), domainHash)),`
|
|
608
|
-
: ` ${d.id}: (payload: ${payloadName}) => holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, payload, domainHash)),`,
|
|
624
|
+
? ` ${d.id}: (payload: ${payloadName}) => holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, stampNow(payload, ${JSON.stringify(stamps)}), domainHash, actor)),`
|
|
625
|
+
: ` ${d.id}: (payload: ${payloadName}) => holon.offer(encodeIntentOffer(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, payload, domainHash, actor)),`,
|
|
609
626
|
);
|
|
610
627
|
}
|
|
611
628
|
|
|
@@ -668,11 +685,19 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
668
685
|
}
|
|
669
686
|
|
|
670
687
|
out.push(
|
|
671
|
-
`/** Typed client for the \`${domain}\` domain, bound to a connected holon
|
|
672
|
-
`
|
|
688
|
+
`/** Typed client for the \`${domain}\` domain, bound to a connected holon.`,
|
|
689
|
+
` * Pass \`{ actor }\` (the verified principal uid) to author AS that user — every write seals it as`,
|
|
690
|
+
` * the captured principal, so the ReBAC \`.requires(...)\` gate + the integrity-dual endorse model`,
|
|
691
|
+
` * judge the real author. Omit it for open (un-gated) domains. */`,
|
|
692
|
+
`export function ${tsClientFactoryName(domain)}(holon: NomosHolon, opts: { domainHash?: string; actor?: string } = {}) {`,
|
|
693
|
+
` const domainHash = opts.domainHash ?? ${hashConst};`,
|
|
694
|
+
` const actor = opts.actor;`,
|
|
673
695
|
` return {`,
|
|
674
696
|
` domain: ${JSON.stringify(domain)} as const,`,
|
|
675
697
|
` domainHash,`,
|
|
698
|
+
...(opts.interface !== undefined
|
|
699
|
+
? [` interface: ${interfaceConstName(opts.packageName)}[${JSON.stringify(domain)}],`]
|
|
700
|
+
: []),
|
|
676
701
|
...methods,
|
|
677
702
|
` };`,
|
|
678
703
|
`}`,
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
*/
|
|
48
48
|
import { execFileSync } from "node:child_process";
|
|
49
49
|
import { expandCapabilityExports, isCapabilityDecl } from "./capability_exports.js";
|
|
50
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
50
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
51
51
|
import { createRequire } from "node:module";
|
|
52
52
|
import path from "node:path";
|
|
53
53
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -103,6 +103,7 @@ import {
|
|
|
103
103
|
} from "./stable_ids.js";
|
|
104
104
|
import type { UsdLayer } from "./usd.js";
|
|
105
105
|
import { generateTsClient, tsClientFactoryName } from "./codegen_ts.js";
|
|
106
|
+
import { domainInterface, type DomainInterface } from "./shape_compat.js";
|
|
106
107
|
import { generateTsProof, proofLegsIndex } from "./codegen_proof.js";
|
|
107
108
|
|
|
108
109
|
const DSL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -149,6 +150,8 @@ interface DomainConfig {
|
|
|
149
150
|
};
|
|
150
151
|
}
|
|
151
152
|
|
|
153
|
+
type DartSupportImport = "vendored" | "package";
|
|
154
|
+
|
|
152
155
|
interface PackageConfig {
|
|
153
156
|
readonly name: string;
|
|
154
157
|
readonly domains: readonly DomainConfig[];
|
|
@@ -159,13 +162,16 @@ interface PackageConfig {
|
|
|
159
162
|
* Typed DART frontend (the Flutter sibling of the generated TS client; #codegen_dart).
|
|
160
163
|
* `false`/absent = skip (default). `true` = emit into `<outDir>/dart/`;
|
|
161
164
|
* `{ out: "relative/dir" }` = emit there (resolved against the config dir).
|
|
165
|
+
* `{ support: "package" }` keeps `package:nomos_dsl/nomos_dsl.dart` imports for Flutter apps that
|
|
166
|
+
* already depend on `nomos_dsl`/`nomos_flutter`. The default `"vendored"` mode preserves the old
|
|
167
|
+
* self-contained directory.
|
|
162
168
|
* Per composed domain one `<name>.dart` (payload classes + read models + typed read
|
|
163
169
|
* accessors via the SAME `generateDartDomain` the co2 tenant emit uses), PLUS the
|
|
164
170
|
* framework `nomos_dsl` Dart support files vendored alongside (the generated files'
|
|
165
171
|
* `package:nomos_dsl/...` import is rewritten to a relative one) — so a Flutter app
|
|
166
172
|
* can vendor the directory as-is, no pub deps.
|
|
167
173
|
*/
|
|
168
|
-
readonly dart?: boolean | { readonly out?: string };
|
|
174
|
+
readonly dart?: boolean | { readonly out?: string; readonly support?: DartSupportImport };
|
|
169
175
|
/**
|
|
170
176
|
* OPT-IN layered USD emission (also `--layered` on the CLI). When true, ALSO emit
|
|
171
177
|
* `build/layers/<domain>--<module>.usda` (+ `.layer.json`) — one REAL USD layer per
|
|
@@ -411,25 +417,41 @@ const DART_SUPPORT_SRC = [
|
|
|
411
417
|
"webview_bridge.dart",
|
|
412
418
|
] as const;
|
|
413
419
|
|
|
420
|
+
function removeVendoredDartSupport(dartOut: string): void {
|
|
421
|
+
rmSync(path.join(dartOut, "nomos_dsl.dart"), { force: true });
|
|
422
|
+
rmSync(path.join(dartOut, "src"), { recursive: true, force: true });
|
|
423
|
+
}
|
|
424
|
+
|
|
414
425
|
/**
|
|
415
426
|
* Emit the typed Dart for the composed domain modules into `dartOut`: one
|
|
416
427
|
* `<name>.dart` per domain (the SAME `generateDartDomain` path the co2 tenant's
|
|
417
428
|
* `emit_dart.ts` drives) + the vendored `nomos_dsl.dart` barrel and its `src/`
|
|
418
429
|
* support files, with the generated files' `package:` import rewritten relative.
|
|
419
430
|
*/
|
|
420
|
-
function emitDart(
|
|
421
|
-
|
|
431
|
+
function emitDart(
|
|
432
|
+
modules: readonly DomainModule[],
|
|
433
|
+
dartOut: string,
|
|
434
|
+
domainHash: string,
|
|
435
|
+
packageInterface: Record<string, DomainInterface> = {},
|
|
436
|
+
supportImport: DartSupportImport = "vendored",
|
|
437
|
+
): string[] {
|
|
438
|
+
mkdirSync(dartOut, { recursive: true });
|
|
422
439
|
|
|
423
|
-
// 1.
|
|
440
|
+
// 1. optional vendored framework support (barrel + src/), copied byte-for-byte except
|
|
424
441
|
// the barrel's schema_validation export (see DART_SUPPORT_SRC).
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
442
|
+
if (supportImport === "vendored") {
|
|
443
|
+
mkdirSync(path.join(dartOut, "src"), { recursive: true });
|
|
444
|
+
const libDir = path.join(DSL_DIR, "dart", "lib");
|
|
445
|
+
const barrel = readFileSync(path.join(libDir, "nomos_dsl.dart"), "utf8")
|
|
446
|
+
.split("\n")
|
|
447
|
+
.filter((line) => !line.includes("src/schema_validation.dart"))
|
|
448
|
+
.join("\n");
|
|
449
|
+
writeFileSync(path.join(dartOut, "nomos_dsl.dart"), barrel, "utf8");
|
|
450
|
+
for (const f of DART_SUPPORT_SRC) {
|
|
451
|
+
copyFileSync(path.join(libDir, "src", f), path.join(dartOut, "src", f));
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
removeVendoredDartSupport(dartOut);
|
|
433
455
|
}
|
|
434
456
|
|
|
435
457
|
// 2. one generated file per composed domain — the TYPES (`<name>.dart`) + the async bridge
|
|
@@ -450,10 +472,18 @@ function emitDart(modules: readonly DomainModule[], dartOut: string, domainHash:
|
|
|
450
472
|
);
|
|
451
473
|
}
|
|
452
474
|
const p = path.join(dartOut, `${mod.name}.dart`);
|
|
453
|
-
writeFileSync(
|
|
475
|
+
writeFileSync(
|
|
476
|
+
p,
|
|
477
|
+
supportImport === "vendored" ? src.replace(DART_PACKAGE_IMPORT, DART_RELATIVE_IMPORT) : src,
|
|
478
|
+
"utf8",
|
|
479
|
+
);
|
|
454
480
|
written.push(p);
|
|
455
481
|
|
|
456
|
-
const clientSrc = generateDartBridgeClient(modForDart, {
|
|
482
|
+
const clientSrc = generateDartBridgeClient(modForDart, {
|
|
483
|
+
domainHash,
|
|
484
|
+
interface: packageInterface[modForDart.domain ?? modForDart.name],
|
|
485
|
+
supportImport,
|
|
486
|
+
});
|
|
457
487
|
const cp = path.join(dartOut, `${mod.name}.client.dart`);
|
|
458
488
|
writeFileSync(cp, clientSrc, "utf8");
|
|
459
489
|
written.push(cp);
|
|
@@ -990,6 +1020,30 @@ async function main(): Promise<void> {
|
|
|
990
1020
|
// identity across clean clones). Deterministic — re-compiles rewrite the same bytes.
|
|
991
1021
|
writeFileSync(lockPath, JSON.stringify(nextLock, null, 2) + "\n", "utf8");
|
|
992
1022
|
|
|
1023
|
+
// ── THE STRUCTURAL INTERFACE (#72) — directives (payload shapes) + aggregates (read
|
|
1024
|
+
// shapes, sid-keyed), keyed by DOMAIN. The SAME object is (a) baked into the generated
|
|
1025
|
+
// clients as their requirement and (b) staged on the deploy as the workspace's offer, so
|
|
1026
|
+
// `connect()` negotiates compatibility by SHAPE (stable-id subtyping), not by law hash.
|
|
1027
|
+
// architecture/shape_compatibility.md. Multiple modules sharing a domain key merge.
|
|
1028
|
+
const packageInterface: Record<string, DomainInterface> = {};
|
|
1029
|
+
for (const mod of domainModules) {
|
|
1030
|
+
const key = mod.domain ?? mod.name;
|
|
1031
|
+
const di = domainInterface(mod, nextLock.domains[mod.name]?.stableIds);
|
|
1032
|
+
const prior = packageInterface[key];
|
|
1033
|
+
packageInterface[key] = prior
|
|
1034
|
+
? {
|
|
1035
|
+
...prior,
|
|
1036
|
+
directives: { ...prior.directives, ...di.directives },
|
|
1037
|
+
aggregates: { ...prior.aggregates, ...di.aggregates },
|
|
1038
|
+
}
|
|
1039
|
+
: di;
|
|
1040
|
+
}
|
|
1041
|
+
writeFileSync(
|
|
1042
|
+
path.join(outDir, `${cfg.name}.interface.json`),
|
|
1043
|
+
JSON.stringify(packageInterface, null, 2) + "\n",
|
|
1044
|
+
"utf8",
|
|
1045
|
+
);
|
|
1046
|
+
|
|
993
1047
|
// ── reports ──
|
|
994
1048
|
const entryReports: string[] = [];
|
|
995
1049
|
for (const [reportId, ref] of Object.entries(cfg.reports ?? {})) {
|
|
@@ -1301,6 +1355,9 @@ async function main(): Promise<void> {
|
|
|
1301
1355
|
// collector's doc above). The worker stages it on the manifest overlay:
|
|
1302
1356
|
// the deploy-time binding warning + the executor's scan map read it there.
|
|
1303
1357
|
...(capabilityLowerings.length > 0 ? { capabilities: capabilityLowerings } : {}),
|
|
1358
|
+
// THE STRUCTURAL OFFER (#72): the per-domain interface, staged on the manifest overlay so
|
|
1359
|
+
// `connect()` pulls the workspace's installed shapes and negotiates by structural subtyping.
|
|
1360
|
+
interface: packageInterface,
|
|
1304
1361
|
};
|
|
1305
1362
|
const deployPath = path.join(outDir, `${cfg.name}.deploy.json`);
|
|
1306
1363
|
writeFileSync(deployPath, JSON.stringify(deployBody) + "\n", "utf8");
|
|
@@ -1350,14 +1407,25 @@ async function main(): Promise<void> {
|
|
|
1350
1407
|
// ── 5. the typed TS client (#10 — the web sibling of codegen_dart), with the
|
|
1351
1408
|
// deployed law's content hash baked in (emitted AFTER the package is hashed) ──
|
|
1352
1409
|
const clientPath = path.join(outDir, `${cfg.name}.client.ts`);
|
|
1353
|
-
writeFileSync(clientPath, generateTsClient(domainModules, { packageName: cfg.name, domainHash }), "utf8");
|
|
1410
|
+
writeFileSync(clientPath, generateTsClient(domainModules, { packageName: cfg.name, domainHash, interface: packageInterface }), "utf8");
|
|
1354
1411
|
|
|
1355
1412
|
// ── 6. the typed Dart frontend (opt-in; the Flutter sibling of step 5, emitted
|
|
1356
1413
|
// after the package hash like the TS client) ──
|
|
1357
1414
|
const dartCfg = cfg.dart ?? false;
|
|
1358
|
-
if (
|
|
1359
|
-
|
|
1415
|
+
if (
|
|
1416
|
+
dartCfg !== false &&
|
|
1417
|
+
dartCfg !== true &&
|
|
1418
|
+
(typeof dartCfg !== "object" ||
|
|
1419
|
+
dartCfg === null ||
|
|
1420
|
+
(dartCfg.out !== undefined && typeof dartCfg.out !== "string") ||
|
|
1421
|
+
(dartCfg.support !== undefined && dartCfg.support !== "vendored" && dartCfg.support !== "package"))
|
|
1422
|
+
) {
|
|
1423
|
+
fail(
|
|
1424
|
+
`config key 'dart' must be false, true, or { out?: "relative/dir", support?: "vendored" | "package" }; got ${JSON.stringify(dartCfg)}`,
|
|
1425
|
+
);
|
|
1360
1426
|
}
|
|
1427
|
+
const dartSupportImport: DartSupportImport =
|
|
1428
|
+
typeof dartCfg === "object" && dartCfg.support !== undefined ? dartCfg.support : "vendored";
|
|
1361
1429
|
let dartOut: string | undefined;
|
|
1362
1430
|
let dartFiles: string[] = [];
|
|
1363
1431
|
if (dartCfg !== false) {
|
|
@@ -1365,7 +1433,7 @@ async function main(): Promise<void> {
|
|
|
1365
1433
|
cfgDir,
|
|
1366
1434
|
typeof dartCfg === "object" && dartCfg.out !== undefined ? dartCfg.out : path.join(outDir, "dart"),
|
|
1367
1435
|
);
|
|
1368
|
-
dartFiles = emitDart(domainModules, dartOut, domainHash);
|
|
1436
|
+
dartFiles = emitDart(domainModules, dartOut, domainHash, packageInterface, dartSupportImport);
|
|
1369
1437
|
}
|
|
1370
1438
|
|
|
1371
1439
|
// ── summary ──
|
|
@@ -1407,7 +1475,9 @@ async function main(): Promise<void> {
|
|
|
1407
1475
|
console.log(` proof SKIPPED — ${proofSkip}`);
|
|
1408
1476
|
}
|
|
1409
1477
|
if (dartOut !== undefined) {
|
|
1410
|
-
|
|
1478
|
+
const supportText =
|
|
1479
|
+
dartSupportImport === "vendored" ? "vendored nomos_dsl support" : "package nomos_dsl imports";
|
|
1480
|
+
console.log(` dart ${rel(dartOut)}${path.sep} (${dartFiles.length} domain file(s) + ${supportText} — typed Dart for Flutter)`);
|
|
1411
1481
|
}
|
|
1412
1482
|
console.log(` deploy ${rel(deployPath)}`);
|
|
1413
1483
|
console.log(``);
|