@githolon/dsl 0.11.0 → 0.12.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 +1 -1
- package/src/codegen_dart.ts +1 -1
- package/src/codegen_dart_client.ts +219 -0
- package/src/compile_package_main.ts +21 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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/codegen_dart.ts
CHANGED
|
@@ -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
|
-
|
|
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 ──
|