@githolon/dsl 0.11.0 → 0.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.11.0",
3
+ "version": "0.13.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",
@@ -1265,7 +1265,7 @@ function parseField(s: AggFieldSpec, aggregate: string): string {
1265
1265
  * `SiteRootAggregate` → `Site`) and append `ReadModel` (`SiteReadModel`,
1266
1266
  * `BuildingReadModel`, `ThingReadModel`, `TrackableThingReadModel`). Pure + deterministic
1267
1267
  * — the same wire type always yields the same name. */
1268
- function readModelClassName(aggregateType: string): string {
1268
+ export function readModelClassName(aggregateType: string): string {
1269
1269
  let stem = aggregateType;
1270
1270
  if (stem.endsWith("RootAggregate")) {
1271
1271
  stem = stem.slice(0, -"RootAggregate".length);
@@ -0,0 +1,219 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
+ // wasm32-wasip1 artifact {kernel · projection · embedded QuickJS engine}.
4
+ // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
5
+
6
+ /**
7
+ * ASYNC DART BRIDGE-CLIENT CODEGEN (#61) — the Flutter sibling of `codegen_ts.ts`.
8
+ *
9
+ * `codegen_dart.ts` emits the per-domain TYPES: read-model classes (`…ReadModel.fromProjected`),
10
+ * typed intent DTOs (`…Payload.toPayloadJson()`), typed ids, enums. Until this file a Flutter app
11
+ * still hand-typed directive ids + query ids against the raw `NomosBridge` (string + Map). This closes
12
+ * that: ONE generated `<name>.client.dart` per domain, emitted by `nomos-compile` AFTER the package is
13
+ * hashed (the deployed law's content hash baked in), carrying a `<Domain>Client` over a connected
14
+ * [NomosBridge] (the WebView runner) with:
15
+ *
16
+ * * a typed DISPATCH method per directive — takes the generated `…Payload`, serialises via its
17
+ * `toPayloadJson()`, auto-stamps omitted `…At` timestamps (the same caller-clock sugar the TS
18
+ * client applies), returns the new local head;
19
+ * * a typed declared-QUERY accessor per `query(...)` — one-shot `Future<List<…ReadModel>>` + a
20
+ * reactive `watch…` `Stream<List<…ReadModel>>`, parsed off the bridge's `{type,id,data}` rows;
21
+ * * by-id read/watch + a type-index `list…` per aggregate;
22
+ * * typed O(1) count/sum accessors.
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.
27
+ */
28
+ import type { z } from "zod";
29
+
30
+ import type { AggregateHandle } from "./aggregate.js";
31
+ import type { Field } from "./fields.js";
32
+ import type { Directive } from "./directive.js";
33
+ import type { QueryDecl } from "./query.js";
34
+ import type { DomainModule } from "./codegen_dart.js";
35
+ import { cap, readModelClassName } from "./codegen_dart.js";
36
+ import { finishCount } from "./count.js";
37
+ import { finishSum } from "./sum.js";
38
+ import { autoStampFields } from "./codegen_ts.js";
39
+
40
+ const camel = (s: string) => s.replace(/[_\s-]+(\w)/g, (_m, c: string) => c.toUpperCase());
41
+ const lcFirst = (s: string) => (s.length ? s[0]!.toLowerCase() + s.slice(1) : s);
42
+
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ type AnyDirective = Directive<any>;
45
+
46
+ /** Dart reserved words that can't be method names — suffixed `_` if one slips through. */
47
+ const DART_RESERVED = new Set([
48
+ "assert", "break", "case", "catch", "class", "const", "continue", "default", "do", "else", "enum",
49
+ "extends", "false", "final", "finally", "for", "if", "in", "is", "new", "null", "rethrow", "return",
50
+ "super", "switch", "this", "throw", "true", "try", "var", "void", "while", "with", "dynamic",
51
+ ]);
52
+ const safeMethod = (s: string): string => (DART_RESERVED.has(s) ? `${s}_` : s);
53
+
54
+ /** `guestbook` / `tool-library` / `Tool Library` → `GuestbookClient` / `ToolLibraryClient`. */
55
+ export const dartClientClassName = (domain: string): string => `${cap(camel(domain))}Client`;
56
+ /** `guestbook` → `guestbookDomainHash` — the Dart top-level const (lowerCamel by lint). */
57
+ export const dartHashConstName = (fileBase: string): string => `${lcFirst(camel(fileBase))}DomainHash`;
58
+
59
+ const dartStr = (s: string) => `'${s.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
60
+
61
+ /** The Dart param type for a query index key (int-kinded keys → `int`, else `String`). */
62
+ function dartKeyType(agg: AggregateHandle | undefined, key: string): string {
63
+ const lead = key.includes(".") ? key.split(".")[0]! : key;
64
+ const f = agg?.fields[lead] as Field | undefined;
65
+ return f && (f.kind === "int" || f.kind === "counter") ? "int" : "String";
66
+ }
67
+
68
+ /** A Dart-safe param identifier from a (possibly dotted) index key: `a.b.c` → `aBC`. */
69
+ function dartKeyParam(key: string): string {
70
+ const parts = key.split(".").filter((p) => p !== "");
71
+ const id = parts.length === 0 ? "key" : parts[0]! + parts.slice(1).map((p) => cap(p)).join("");
72
+ const cleaned = id.replace(/[^A-Za-z0-9_]/g, "_");
73
+ const safe = /^[0-9]/.test(cleaned) ? `v${cleaned}` : cleaned;
74
+ return safeMethod(safe);
75
+ }
76
+
77
+ /** Parse ONE bridge row (`{type,id,data}`) into a typed read model. Works for a `Map` (one-shot
78
+ * `query`/`queryById` rows) or a `dynamic` (`watch` stream rows) — indexing resolves on both. */
79
+ function rowToModel(model: string, rowVar: string): string {
80
+ return `${model}.fromProjected((${rowVar}['data'] as Map).cast<String, dynamic>(), (${rowVar}['id'] ?? '').toString())`;
81
+ }
82
+
83
+ export interface DartBridgeClientOptions {
84
+ /** sha256 of the built `.package.usda` — the law this client dispatches under. */
85
+ readonly domainHash: string;
86
+ }
87
+
88
+ /** Generate the `<name>.client.dart` async bridge client for one domain module. */
89
+ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClientOptions): string {
90
+ const domain = mod.domain ?? mod.name;
91
+ const className = dartClientClassName(domain);
92
+ const hashConst = dartHashConstName(mod.name);
93
+ const aggById = new Map<string, AggregateHandle>();
94
+ for (const a of mod.aggregates) aggById.set(a.id, a);
95
+
96
+ const methods: string[] = [];
97
+
98
+ // ── directives → typed dispatch ──
99
+ const dirs = (mod.directives as AnyDirective[]).filter(
100
+ (d) => d && typeof d.id === "string" && typeof (d as { payloadSchema?: unknown }).payloadSchema === "object",
101
+ );
102
+ for (const d of dirs) {
103
+ const payloadClass = `${cap(d.id)}Payload`;
104
+ const stamps = autoStampFields((d as unknown as { payloadSchema: z.ZodTypeAny }).payloadSchema);
105
+ const method = safeMethod(d.id);
106
+ if (stamps.length > 0) {
107
+ methods.push(
108
+ ` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
109
+ ` /// Omitted ${stamps.map((s) => `\`${s}\``).join(" / ")} auto-stamp(s) ISO now() at dispatch.`,
110
+ ` Future<String> ${method}(${payloadClass} payload) {`,
111
+ ` final data = payload.toPayloadJson();`,
112
+ ` final now = DateTime.now().toUtc().toIso8601String();`,
113
+ ...stamps.map((s) => ` data.putIfAbsent(${dartStr(s)}, () => now);`),
114
+ ` return bridge.dispatch(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: data, domainHash: domainHash);`,
115
+ ` }`,
116
+ ``,
117
+ );
118
+ } else {
119
+ methods.push(
120
+ ` /// Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head.`,
121
+ ` Future<String> ${method}(${payloadClass} payload) =>`,
122
+ ` bridge.dispatch(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, payload: payload.toPayloadJson(), domainHash: domainHash);`,
123
+ ``,
124
+ );
125
+ }
126
+ }
127
+
128
+ // ── declared queries → one-shot + reactive ──
129
+ for (const q of (mod.queries ?? []) as QueryDecl[]) {
130
+ const model = readModelClassName(q.returns);
131
+ const agg = aggById.get(q.returns);
132
+ const params = q.key.map((k) => `required ${dartKeyType(agg, k)} ${dartKeyParam(k)}`).join(", ");
133
+ const argMap = `{${q.key.map((k) => `${dartStr(k)}: ${dartKeyParam(k)}`).join(", ")}}`;
134
+ const m = safeMethod(lcFirst(camel(q.id)));
135
+ methods.push(
136
+ ` /// Declared query \`${q.id}\` (indexed probe — routed by the read manifest).`,
137
+ ` Future<List<${model}>> ${m}({${params}}) async =>`,
138
+ ` (await bridge.query(${dartStr(q.id)}, ${argMap})).map((r) => ${rowToModel(model, "r")}).toList();`,
139
+ ``,
140
+ ` /// Reactive \`${q.id}\` — re-emits on every local change (cancel the subscription to stop).`,
141
+ ` Stream<List<${model}>> watch${cap(camel(q.id))}({${params}}) =>`,
142
+ ` bridge.watch(${dartStr(q.id)}, ${argMap}).map((rows) => rows.map((r) => ${rowToModel(model, "r")}).toList());`,
143
+ ``,
144
+ );
145
+ }
146
+
147
+ // ── counts / sums → O(1) typed reads ──
148
+ for (const c of (mod.counts ?? []).map((raw) => finishCount(raw))) {
149
+ const m = safeMethod(lcFirst(camel(c.id)));
150
+ methods.push(
151
+ c.by != null
152
+ ? ` /// Declared count \`${c.id}\` — the maintained O(1) tally per \`${c.by}\`.\n Future<int> ${m}(String ${dartKeyParam(c.by)}) => bridge.count(${dartStr(c.id)}, ${dartKeyParam(c.by)});`
153
+ : ` /// Declared count \`${c.id}\` — the maintained O(1) grand total.\n Future<int> ${m}() => bridge.count(${dartStr(c.id)});`,
154
+ ``,
155
+ );
156
+ }
157
+ for (const s of (mod.sums ?? []).map((raw) => finishSum(raw))) {
158
+ const m = safeMethod(lcFirst(camel(s.id)));
159
+ methods.push(
160
+ s.by != null
161
+ ? ` /// Declared sum \`${s.id}\` — the maintained O(1) total per \`${s.by}\`.\n Future<int> ${m}(String ${dartKeyParam(s.by)}) => bridge.sum(${dartStr(s.id)}, ${dartKeyParam(s.by)});`
162
+ : ` /// Declared sum \`${s.id}\` — the maintained O(1) grand total.\n Future<int> ${m}() => bridge.sum(${dartStr(s.id)});`,
163
+ ``,
164
+ );
165
+ }
166
+
167
+ // ── per-aggregate by-id read/watch + type-index list ──
168
+ for (const agg of mod.aggregates) {
169
+ const model = readModelClassName(agg.id);
170
+ const lc = lcFirst(camel(agg.id));
171
+ const Cap = cap(camel(agg.id));
172
+ methods.push(
173
+ ` /// Read one ${agg.id} by aggregate id.`,
174
+ ` Future<List<${model}>> ${safeMethod(`${lc}ById`)}(String id) async =>`,
175
+ ` (await bridge.queryById(id)).map((r) => ${rowToModel(model, "r")}).toList();`,
176
+ ``,
177
+ ` /// Watch one ${agg.id} by aggregate id (local reactive read).`,
178
+ ` Stream<List<${model}>> watch${Cap}ById(String id) =>`,
179
+ ` bridge.watchById(id).map((rows) => rows.map((r) => ${rowToModel(model, "r")}).toList());`,
180
+ ``,
181
+ ` /// Every ${agg.id} (the type-index probe — O(result), not a scan).`,
182
+ ` Future<List<${model}>> ${safeMethod(`list${Cap}s`)}() async =>`,
183
+ ` (await bridge.query('aggregatesByType', {'__type': ${dartStr(agg.id)}})).map((r) => ${rowToModel(model, "r")}).toList();`,
184
+ ``,
185
+ );
186
+ }
187
+
188
+ return [
189
+ `// AUTO-GENERATED by nomos-compile — typed ASYNC Dart client for domain "${domain}".`,
190
+ `// Drives the REAL githolon over a NomosBridge (the WebView runner). Regenerated every compile; do`,
191
+ `// not edit. Construct with a connected bridge:`,
192
+ `//`,
193
+ `// final bridge = NomosBridge(transport); // transport = your WebViewTransport`,
194
+ `// await bridge.ready;`,
195
+ `// await bridge.connect(cloud: cloud, workspace: ws, clientId: deviceId);`,
196
+ `// final app = ${className}(bridge);`,
197
+ `// await app.${dirs[0] ? safeMethod(dirs[0].id) : "someDirective"}(/* typed payload */);`,
198
+ ``,
199
+ `import 'nomos_dsl.dart';`,
200
+ `import '${mod.name}.dart';`,
201
+ `// Re-export the sibling types (read models + payloads + the bridge runtime) so ONE import of this`,
202
+ `// client file gives an app the whole typed surface.`,
203
+ `export '${mod.name}.dart';`,
204
+ `export 'nomos_dsl.dart' show NomosBridge, BridgeTransport, NomosBridgeError, kWebviewBridgeVersion;`,
205
+ ``,
206
+ `/// sha256 of the deployed \`.package.usda\` — the installed law this client dispatches under.`,
207
+ `const String ${hashConst} = ${dartStr(opts.domainHash)};`,
208
+ ``,
209
+ `/// Typed async client for the \`${domain}\` domain, bound to a connected [NomosBridge].`,
210
+ `class ${className} {`,
211
+ ` final NomosBridge bridge;`,
212
+ ` final String domainHash;`,
213
+ ` ${className}(this.bridge, {this.domainHash = ${hashConst}});`,
214
+ ``,
215
+ ...methods,
216
+ `}`,
217
+ ``,
218
+ ].join("\n");
219
+ }
@@ -63,6 +63,7 @@ import {
63
63
  type DomainModule,
64
64
  type PermissionVocabulary,
65
65
  } from "./codegen_dart.js";
66
+ import { generateDartBridgeClient } from "./codegen_dart_client.js";
66
67
  import type { AnyCount } from "./count.js";
67
68
  import type { QueryDecl } from "./query.js";
68
69
  import type { SpatialDecl } from "./spatial.js";
@@ -408,6 +409,9 @@ const DART_SUPPORT_SRC = [
408
409
  "dispatch.dart",
409
410
  "subscriptions.dart",
410
411
  "provider.dart",
412
+ // The async WebView bridge runtime (NomosBridge / BridgeTransport) the generated
413
+ // `<name>.client.dart` drives — the barrel re-exports it, so it must be vendored too.
414
+ "webview_bridge.dart",
411
415
  ] as const;
412
416
 
413
417
  /**
@@ -416,7 +420,7 @@ const DART_SUPPORT_SRC = [
416
420
  * `emit_dart.ts` drives) + the vendored `nomos_dsl.dart` barrel and its `src/`
417
421
  * support files, with the generated files' `package:` import rewritten relative.
418
422
  */
419
- function emitDart(modules: readonly DomainModule[], dartOut: string): string[] {
423
+ function emitDart(modules: readonly DomainModule[], dartOut: string, domainHash: string): string[] {
420
424
  mkdirSync(path.join(dartOut, "src"), { recursive: true });
421
425
 
422
426
  // 1. the vendored framework support (barrel + src/), copied byte-for-byte except
@@ -431,10 +435,18 @@ function emitDart(modules: readonly DomainModule[], dartOut: string): string[] {
431
435
  copyFileSync(path.join(libDir, "src", f), path.join(dartOut, "src", f));
432
436
  }
433
437
 
434
- // 2. one generated file per composed domain.
438
+ // 2. one generated file per composed domain — the TYPES (`<name>.dart`) + the async bridge
439
+ // CLIENT (`<name>.client.dart`, the Flutter sibling of `<name>.client.ts`).
435
440
  const written: string[] = [];
436
441
  for (const mod of modules) {
437
- const src = generateDartDomain(mod);
442
+ // Default the read-model allowlist to EVERY aggregate when the domain didn't pin one, so
443
+ // `<name>.dart` carries a read model per aggregate — the bridge client imports them. A domain
444
+ // that pins `readModelAggregates` keeps its choice (byte-identical to before this default).
445
+ const modForDart: DomainModule =
446
+ mod.readModelAggregates === undefined
447
+ ? { ...mod, readModelAggregates: mod.aggregates.map((a) => a.id) }
448
+ : mod;
449
+ const src = generateDartDomain(modForDart);
438
450
  if (!src.includes(DART_PACKAGE_IMPORT)) {
439
451
  fail(
440
452
  `generated Dart for domain '${mod.name}' does not carry the expected support import (${DART_PACKAGE_IMPORT}) — codegen_dart header drifted; fix the rewrite in compile_package_main.ts`,
@@ -443,6 +455,11 @@ function emitDart(modules: readonly DomainModule[], dartOut: string): string[] {
443
455
  const p = path.join(dartOut, `${mod.name}.dart`);
444
456
  writeFileSync(p, src.replace(DART_PACKAGE_IMPORT, DART_RELATIVE_IMPORT), "utf8");
445
457
  written.push(p);
458
+
459
+ const clientSrc = generateDartBridgeClient(modForDart, { domainHash });
460
+ const cp = path.join(dartOut, `${mod.name}.client.dart`);
461
+ writeFileSync(cp, clientSrc, "utf8");
462
+ written.push(cp);
446
463
  }
447
464
  return written;
448
465
  }
@@ -1351,7 +1368,7 @@ async function main(): Promise<void> {
1351
1368
  cfgDir,
1352
1369
  typeof dartCfg === "object" && dartCfg.out !== undefined ? dartCfg.out : path.join(outDir, "dart"),
1353
1370
  );
1354
- dartFiles = emitDart(domainModules, dartOut);
1371
+ dartFiles = emitDart(domainModules, dartOut, domainHash);
1355
1372
  }
1356
1373
 
1357
1374
  // ── summary ──