@githolon/dsl 0.1.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/LICENSE.md +36 -0
- package/compile_package.mjs +50 -0
- package/package.json +59 -0
- package/src/aggregate.ts +167 -0
- package/src/authoring.ts +119 -0
- package/src/build_package.ts +636 -0
- package/src/certified_read.ts +313 -0
- package/src/codegen_dart.ts +2732 -0
- package/src/codegen_dot.ts +466 -0
- package/src/codegen_provider_dart.ts +358 -0
- package/src/codegen_ts.ts +365 -0
- package/src/codegen_usda.ts +388 -0
- package/src/combined.ts +195 -0
- package/src/compile_engine.ts +567 -0
- package/src/compile_package_main.ts +496 -0
- package/src/compose.ts +317 -0
- package/src/count.ts +218 -0
- package/src/ctx.ts +57 -0
- package/src/derived.ts +138 -0
- package/src/directive.ts +306 -0
- package/src/drivers.ts +95 -0
- package/src/emits_guard.ts +123 -0
- package/src/engine_entry.ts +449 -0
- package/src/exists.ts +170 -0
- package/src/extremum.ts +227 -0
- package/src/fields.ts +291 -0
- package/src/framework/bootstrap.ts +22 -0
- package/src/framework/disclosure.ts +108 -0
- package/src/framework/domain_lifecycle.ts +108 -0
- package/src/framework/identity.ts +537 -0
- package/src/framework/impure_capability.ts +643 -0
- package/src/framework/rbac.ts +418 -0
- package/src/framework/repair.ts +150 -0
- package/src/framework/sync_lifecycle.ts +125 -0
- package/src/framework/workspace_invariant.ts +128 -0
- package/src/framework/workspaces.ts +817 -0
- package/src/index.ts +317 -0
- package/src/manifest.ts +947 -0
- package/src/ops.ts +145 -0
- package/src/ordered_read.ts +228 -0
- package/src/predicate.ts +203 -0
- package/src/query/compile.ts +0 -0
- package/src/query/relations.ts +144 -0
- package/src/query.ts +151 -0
- package/src/read.ts +54 -0
- package/src/relation.ts +189 -0
- package/src/report/csv.ts +54 -0
- package/src/report.ts +401 -0
- package/src/spatial.ts +115 -0
- package/src/sum.ts +194 -0
- package/src/usd.ts +563 -0
- package/src/wire.ts +149 -0
- package/src/wire_encode.ts +250 -0
|
@@ -0,0 +1,365 @@
|
|
|
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
|
|
4
|
+
// QuickJS engine} on V8 + WASI-shim, byte-identical everywhere. V8 = portability; the one
|
|
5
|
+
// wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
|
|
6
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* TS CLIENT CODEGEN (#10) — the WEB sibling of `codegen_dart.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Dart apps get generated payload classes / read models / typed query accessors;
|
|
12
|
+
* until this file, a web app on `@githolon/client` hand-typed directive ids and
|
|
13
|
+
* payload shapes against the raw `dispatch()` surface. `generateTsClient(modules,
|
|
14
|
+
* opts)` closes that: ONE generated `.ts` file per package, emitted by
|
|
15
|
+
* `nomos-compile` AFTER the package is hashed (the deployed law's content hash is
|
|
16
|
+
* baked in as a constant), carrying:
|
|
17
|
+
*
|
|
18
|
+
* * a PAYLOAD TYPE per directive — derived from the SAME zod schema the sealed
|
|
19
|
+
* engine validates (recursive zod→TS: enums become literal unions, `.optional()`
|
|
20
|
+
* becomes `?`, `.int()` stays `number`), so a payload the type system accepts is
|
|
21
|
+
* a payload the engine accepts;
|
|
22
|
+
* * a READ-MODEL interface per aggregate — derived from the DSL `Field` kinds the
|
|
23
|
+
* projection decodes by (set→`T[]`, map→`Record<string, V>` via `mapValueKind`,
|
|
24
|
+
* `t.jsonObject()`→`Record<string, unknown>`, plain `t.json()`→`unknown`), with
|
|
25
|
+
* derived/combined read fields merged in (typed off their declared zod returns).
|
|
26
|
+
* EVERY field is `?`: a projection row carries only what has folded so far;
|
|
27
|
+
* * a CLIENT FACTORY per domain — typed `dispatch` wrappers bound to the baked
|
|
28
|
+
* domainHash, typed declared-query accessors, and by-id read/watch helpers —
|
|
29
|
+
* over a STRUCTURAL `NomosHolon` interface, so the file has ZERO imports and
|
|
30
|
+
* binds to `@githolon/client`'s `connect()` result (browser or node) as-is.
|
|
31
|
+
*
|
|
32
|
+
* Like every emitter here: NO FALLBACK. An unsupported zod shape throws at compile
|
|
33
|
+
* time, never degrades to `any` silently.
|
|
34
|
+
*/
|
|
35
|
+
import type { z } from "zod";
|
|
36
|
+
|
|
37
|
+
import type { AggregateHandle } from "./aggregate.js";
|
|
38
|
+
import type { Field } from "./fields.js";
|
|
39
|
+
import type { Directive } from "./directive.js";
|
|
40
|
+
import type { DomainModule } from "./codegen_dart.js";
|
|
41
|
+
import type { QueryDecl } from "./query.js";
|
|
42
|
+
import { finishCount } from "./count.js";
|
|
43
|
+
import { finishSum } from "./sum.js";
|
|
44
|
+
import type { DerivedDecl } from "./derived.js";
|
|
45
|
+
import type { CombinedDecl } from "./combined.js";
|
|
46
|
+
|
|
47
|
+
const cap = (s: string) => (s.length ? s[0]!.toUpperCase() + s.slice(1) : s);
|
|
48
|
+
const camel = (s: string) => s.replace(/[_-]+(\w)/g, (_m, c: string) => c.toUpperCase());
|
|
49
|
+
const lcFirst = (s: string) => (s.length ? s[0]!.toLowerCase() + s.slice(1) : s);
|
|
50
|
+
const upperSnake = (s: string) => s.replace(/[-\s]+/g, "_").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
|
|
51
|
+
|
|
52
|
+
// ── zod → TS (the same `_def ?? def` convention as codegen_dart's walker) ─────────
|
|
53
|
+
|
|
54
|
+
type ZodInternalDef = {
|
|
55
|
+
type?: string | z.ZodTypeAny;
|
|
56
|
+
innerType?: z.ZodTypeAny;
|
|
57
|
+
element?: z.ZodTypeAny;
|
|
58
|
+
shape?: Record<string, z.ZodTypeAny> | (() => Record<string, z.ZodTypeAny>);
|
|
59
|
+
entries?: Record<string, string | number>;
|
|
60
|
+
options?: z.ZodTypeAny[];
|
|
61
|
+
value?: unknown;
|
|
62
|
+
values?: readonly unknown[];
|
|
63
|
+
valueType?: z.ZodTypeAny;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function zodDef(zt: z.ZodTypeAny): ZodInternalDef {
|
|
67
|
+
const raw = zt as unknown as { _def?: ZodInternalDef; def?: ZodInternalDef };
|
|
68
|
+
return raw._def ?? raw.def ?? {};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function zodKind(zt: z.ZodTypeAny): string {
|
|
72
|
+
const t = zodDef(zt).type;
|
|
73
|
+
return typeof t === "string" ? t : "unknown";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function zodEnumValues(zt: z.ZodTypeAny): string[] {
|
|
77
|
+
const raw = zt as unknown as { options?: readonly unknown[] };
|
|
78
|
+
if (raw.options !== undefined) return raw.options.map(String);
|
|
79
|
+
const entries = zodDef(zt).entries;
|
|
80
|
+
if (entries !== undefined) return Object.values(entries).map(String);
|
|
81
|
+
throw new Error("codegen-ts: ZodEnum has no values");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function zodObjectShape(zt: z.ZodTypeAny): Record<string, z.ZodTypeAny> {
|
|
85
|
+
const shape = zodDef(zt).shape;
|
|
86
|
+
if (typeof shape === "function") return shape();
|
|
87
|
+
if (shape !== undefined) return shape;
|
|
88
|
+
const raw = zt as unknown as { shape?: Record<string, z.ZodTypeAny> };
|
|
89
|
+
if (raw.shape !== undefined) return raw.shape;
|
|
90
|
+
throw new Error("codegen-ts: ZodObject has no shape");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function zodArrayElement(zt: z.ZodTypeAny): z.ZodTypeAny {
|
|
94
|
+
const def = zodDef(zt);
|
|
95
|
+
if (def.element !== undefined) return def.element;
|
|
96
|
+
if (typeof def.type !== "string" && def.type !== undefined) return def.type;
|
|
97
|
+
throw new Error("codegen-ts: ZodArray has no element type");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const union = (parts: string[]) => [...new Set(parts)].join(" | ");
|
|
101
|
+
const paren = (t: string) => (t.includes("|") || t.includes("&") ? `(${t})` : t);
|
|
102
|
+
|
|
103
|
+
/** Recursive zod → TS type text. NO FALLBACK: an unhandled shape throws. */
|
|
104
|
+
function tsTypeOfZod(zt: z.ZodTypeAny, indent: string): string {
|
|
105
|
+
switch (zodKind(zt)) {
|
|
106
|
+
case "string":
|
|
107
|
+
return "string";
|
|
108
|
+
case "number":
|
|
109
|
+
return "number"; // `.int()` is a runtime constraint; TS has one number type
|
|
110
|
+
case "boolean":
|
|
111
|
+
return "boolean";
|
|
112
|
+
case "enum":
|
|
113
|
+
return union(zodEnumValues(zt).map((v) => JSON.stringify(v)));
|
|
114
|
+
case "literal": {
|
|
115
|
+
const def = zodDef(zt);
|
|
116
|
+
const v = def.value !== undefined ? def.value : def.values?.[0];
|
|
117
|
+
return JSON.stringify(v);
|
|
118
|
+
}
|
|
119
|
+
case "array":
|
|
120
|
+
return `${paren(tsTypeOfZod(zodArrayElement(zt), indent))}[]`;
|
|
121
|
+
case "object": {
|
|
122
|
+
const shape = zodObjectShape(zt);
|
|
123
|
+
const inner = indent + " ";
|
|
124
|
+
const fields = Object.entries(shape).map(([name, raw]) => {
|
|
125
|
+
let f = raw as z.ZodTypeAny;
|
|
126
|
+
let optional = false;
|
|
127
|
+
while (zodKind(f) === "optional" || zodKind(f) === "default") {
|
|
128
|
+
optional = true;
|
|
129
|
+
f = zodDef(f).innerType!;
|
|
130
|
+
}
|
|
131
|
+
return `${inner}${name}${optional ? "?" : ""}: ${tsTypeOfZod(f, inner)};`;
|
|
132
|
+
});
|
|
133
|
+
return fields.length ? `{\n${fields.join("\n")}\n${indent}}` : "Record<string, never>";
|
|
134
|
+
}
|
|
135
|
+
case "record": {
|
|
136
|
+
const vt = zodDef(zt).valueType;
|
|
137
|
+
return `Record<string, ${vt ? tsTypeOfZod(vt, indent) : "unknown"}>`;
|
|
138
|
+
}
|
|
139
|
+
case "optional":
|
|
140
|
+
case "default":
|
|
141
|
+
return `${tsTypeOfZod(zodDef(zt).innerType!, indent)} | undefined`;
|
|
142
|
+
case "nullable":
|
|
143
|
+
return `${tsTypeOfZod(zodDef(zt).innerType!, indent)} | null`;
|
|
144
|
+
case "union":
|
|
145
|
+
return union((zodDef(zt).options ?? []).map((o) => tsTypeOfZod(o, indent)));
|
|
146
|
+
case "unknown":
|
|
147
|
+
case "any":
|
|
148
|
+
return "unknown";
|
|
149
|
+
default:
|
|
150
|
+
throw new Error(`codegen-ts: unsupported zod shape '${zodKind(zt)}' — extend tsTypeOfZod`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── DSL Field → read-model TS type (the projection's decode surface) ──────────────
|
|
155
|
+
|
|
156
|
+
function tsTypeOfField(field: Field): string {
|
|
157
|
+
switch (field.kind) {
|
|
158
|
+
case "string":
|
|
159
|
+
return "string";
|
|
160
|
+
case "int":
|
|
161
|
+
return "number";
|
|
162
|
+
case "enum":
|
|
163
|
+
try {
|
|
164
|
+
return union(zodEnumValues(field.zod as z.ZodTypeAny).map((v) => JSON.stringify(v)));
|
|
165
|
+
} catch {
|
|
166
|
+
return "string";
|
|
167
|
+
}
|
|
168
|
+
case "ref":
|
|
169
|
+
return "string"; // the related aggregate's id
|
|
170
|
+
case "json":
|
|
171
|
+
return field.jsonShape === "object" ? "Record<string, unknown>" : "unknown";
|
|
172
|
+
case "set":
|
|
173
|
+
return "string[]"; // `t.set(t.string())` is the one set shape (fields.ts)
|
|
174
|
+
case "map": {
|
|
175
|
+
const v =
|
|
176
|
+
field.mapValueKind === "string" ? "string"
|
|
177
|
+
: field.mapValueKind === "int" ? "number"
|
|
178
|
+
: "unknown";
|
|
179
|
+
return `Record<string, ${v}>`;
|
|
180
|
+
}
|
|
181
|
+
default:
|
|
182
|
+
throw new Error(`codegen-ts: unsupported field kind '${field.kind}' — extend tsTypeOfField`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** A loose TS type for a derived/combined field's declared zod `returns`. */
|
|
187
|
+
function tsTypeOfReturns(returns: unknown): string {
|
|
188
|
+
try {
|
|
189
|
+
return tsTypeOfZod(returns as z.ZodTypeAny, " ");
|
|
190
|
+
} catch {
|
|
191
|
+
return "unknown";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ── duck-typed scans (the same shapes the engine entry + compiler discover) ───────
|
|
196
|
+
|
|
197
|
+
function aggregatesOf(mod: Record<string, unknown>): AggregateHandle[] {
|
|
198
|
+
const out: AggregateHandle[] = [];
|
|
199
|
+
for (const v of Object.values(mod)) {
|
|
200
|
+
if (v && typeof v === "object" && (v as { __isAggregateHandle?: boolean }).__isAggregateHandle === true) {
|
|
201
|
+
out.push(v as AggregateHandle);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ── the generator ─────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
export interface TsClientOptions {
|
|
210
|
+
/** The package name (`nomos.package.mjs` `name`) — used for the hash constant. */
|
|
211
|
+
readonly packageName: string;
|
|
212
|
+
/** sha256 of the built `.package.usda` — the law this client dispatches under. */
|
|
213
|
+
readonly domainHash: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function generateTsClient(modules: readonly DomainModule[], opts: TsClientOptions): string {
|
|
217
|
+
const hashConst = `${upperSnake(opts.packageName)}_DOMAIN_HASH`;
|
|
218
|
+
const out: string[] = [];
|
|
219
|
+
out.push(
|
|
220
|
+
`// AUTO-GENERATED by nomos-compile — typed TS client for package "${opts.packageName}".`,
|
|
221
|
+
`// Regenerated on every compile; do not edit. Zero imports: bind it to any holon`,
|
|
222
|
+
`// exposing the @githolon/client surface (the structural NomosHolon below), e.g.`,
|
|
223
|
+
`//`,
|
|
224
|
+
`// import { connect } from "@githolon/client";`,
|
|
225
|
+
`// import { ${lcFirst(camel(modules[0]?.domain ?? "domain"))}Client } from "./build/${opts.packageName}.client.js";`,
|
|
226
|
+
`// const holon = await connect({ cloud, workspace, clientId });`,
|
|
227
|
+
`// const app = ${lcFirst(camel(modules[0]?.domain ?? "domain"))}Client(holon);`,
|
|
228
|
+
``,
|
|
229
|
+
`/** sha256 of the deployed \`.package.usda\` — the installed law this client dispatches under. */`,
|
|
230
|
+
`export const ${hashConst} = ${JSON.stringify(opts.domainHash)};`,
|
|
231
|
+
``,
|
|
232
|
+
`/** One projection row: the aggregate's wire type, its id, and the folded fields. */`,
|
|
233
|
+
`export interface Row<T> { type: string; id: string; data: T }`,
|
|
234
|
+
``,
|
|
235
|
+
`/** The holon surface this client binds to (structurally satisfied by @githolon/client). */`,
|
|
236
|
+
`export interface NomosHolon {`,
|
|
237
|
+
` dispatch(domain: string, directiveId: string, payload: unknown, opts: { domainHash: string }): Promise<string>;`,
|
|
238
|
+
` query(queryId: string, params?: Record<string, unknown>): Promise<unknown>;`,
|
|
239
|
+
` queryById(aggregateId: string): Promise<unknown>;`,
|
|
240
|
+
` watchById(aggregateId: string, cb: (rows: unknown) => void, opts?: { intervalMs?: number }): () => void;`,
|
|
241
|
+
` /** Declared-count / declared-sum O(1) reads (maintained by the read engine). */`,
|
|
242
|
+
` count(countId: string, groupKey?: string): Promise<number>;`,
|
|
243
|
+
` sum(sumId: string, groupKey?: string): Promise<number>;`,
|
|
244
|
+
` /** The DEAD-LETTER QUEUE — refused-but-legitimate intents the app must handle`,
|
|
245
|
+
` * explicitly (inspect → retry after a law fix → or discard). Work is never lost. */`,
|
|
246
|
+
` deadLetters(): Promise<{ id?: string; oid: string; source: string; error: string; domain?: string | null; directiveId?: string | null; at: string }[]>;`,
|
|
247
|
+
` retryDeadLetter(id: string): Promise<{ ok: boolean; head?: string; alreadyOnMain?: boolean; error?: string }>;`,
|
|
248
|
+
` discardDeadLetter(id: string): Promise<{ ok: boolean; removed: number }>;`,
|
|
249
|
+
`}`,
|
|
250
|
+
``,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// ── read models: one interface per aggregate wire type (deduped), deriveds/combineds merged ──
|
|
254
|
+
const aggByType = new Map<string, AggregateHandle>();
|
|
255
|
+
const extrasByType = new Map<string, string[]>();
|
|
256
|
+
for (const mod of modules) {
|
|
257
|
+
for (const agg of mod.aggregates as AggregateHandle[]) aggByType.set(agg.id, agg);
|
|
258
|
+
for (const d of (mod.deriveds ?? []) as DerivedDecl[]) {
|
|
259
|
+
(extrasByType.get(d.of) ?? extrasByType.set(d.of, []).get(d.of)!).push(
|
|
260
|
+
` /** derived read field (engine-projected) */\n ${d.id}?: ${tsTypeOfReturns(d.returns)};`,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
for (const c of (mod.combineds ?? []) as CombinedDecl[]) {
|
|
264
|
+
(extrasByType.get(c.of) ?? extrasByType.set(c.of, []).get(c.of)!).push(
|
|
265
|
+
` /** combined read field (engine-projected via \`${c.refField}\` → ${c.reads}) */\n ${c.id}?: ${tsTypeOfReturns(c.returns)};`,
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
out.push(`// ── read models (projection \`data\` shapes; every field is partial-fold optional) ──`, ``);
|
|
270
|
+
for (const [typeId, agg] of [...aggByType].sort(([a], [b]) => a.localeCompare(b))) {
|
|
271
|
+
out.push(`export interface ${typeId}Data {`);
|
|
272
|
+
for (const [name, field] of Object.entries(agg.fields)) {
|
|
273
|
+
out.push(` ${name}?: ${tsTypeOfField(field as Field)};`);
|
|
274
|
+
}
|
|
275
|
+
for (const extra of extrasByType.get(typeId) ?? []) out.push(extra);
|
|
276
|
+
out.push(`}`, ``);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── payload types + per-domain client factories ──
|
|
280
|
+
const usedPayloadNames = new Set<string>();
|
|
281
|
+
for (const mod of modules) {
|
|
282
|
+
const domain = mod.domain ?? mod.name;
|
|
283
|
+
const dirs = (mod.directives as Directive<unknown>[]).filter(
|
|
284
|
+
(d) => d && typeof d.id === "string" && typeof (d as { payloadSchema?: unknown }).payloadSchema === "object",
|
|
285
|
+
);
|
|
286
|
+
out.push(`// ── domain "${domain}" ──`, ``);
|
|
287
|
+
|
|
288
|
+
const methods: string[] = [];
|
|
289
|
+
for (const d of dirs) {
|
|
290
|
+
let payloadName = `${cap(d.id)}Payload`;
|
|
291
|
+
if (usedPayloadNames.has(payloadName)) payloadName = `${cap(camel(domain))}${cap(d.id)}Payload`;
|
|
292
|
+
usedPayloadNames.add(payloadName);
|
|
293
|
+
const schema = (d as unknown as { payloadSchema: z.ZodTypeAny }).payloadSchema;
|
|
294
|
+
out.push(
|
|
295
|
+
`/** \`${domain}/${d.id}\` payload (mirrors the engine's zod schema). */`,
|
|
296
|
+
`export type ${payloadName} = ${tsTypeOfZod(schema, "")};`,
|
|
297
|
+
``,
|
|
298
|
+
);
|
|
299
|
+
methods.push(
|
|
300
|
+
` /** Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head. */`,
|
|
301
|
+
` ${d.id}: (payload: ${payloadName}) => holon.dispatch(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, payload, { domainHash }),`,
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
for (const q of (mod.queries ?? []) as QueryDecl[]) {
|
|
306
|
+
const agg = aggByType.get(q.returns);
|
|
307
|
+
const params = q.key
|
|
308
|
+
.map((k) => {
|
|
309
|
+
const lead = k.includes(".") ? k.split(".")[0]! : k;
|
|
310
|
+
const f = agg?.fields[lead] as Field | undefined;
|
|
311
|
+
const t = f ? (f.kind === "int" ? "number" : "string") : "string";
|
|
312
|
+
return `${JSON.stringify(k)}: ${t}`;
|
|
313
|
+
})
|
|
314
|
+
.join("; ");
|
|
315
|
+
methods.push(
|
|
316
|
+
` /** Declared query \`${q.id}\` (indexed probe — routed by the read manifest). */`,
|
|
317
|
+
` ${lcFirst(camel(q.id))}: (params: { ${params} }) => holon.query(${JSON.stringify(q.id)}, params) as Promise<Row<${q.returns}Data>[]>,`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
for (const c of (mod.counts ?? []).map((raw) => finishCount(raw))) {
|
|
322
|
+
const grouped = c.by != null;
|
|
323
|
+
methods.push(
|
|
324
|
+
` /** Declared count \`${c.id}\` — the maintained O(1) ${grouped ? `tally per \`${c.by}\`` : "grand total"}. */`,
|
|
325
|
+
grouped
|
|
326
|
+
? ` ${lcFirst(camel(c.id))}: (${c.by}: string) => holon.count(${JSON.stringify(c.id)}, ${c.by}),`
|
|
327
|
+
: ` ${lcFirst(camel(c.id))}: () => holon.count(${JSON.stringify(c.id)}),`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
for (const s of (mod.sums ?? []).map((raw) => finishSum(raw))) {
|
|
331
|
+
const grouped = s.by != null;
|
|
332
|
+
methods.push(
|
|
333
|
+
` /** Declared sum \`${s.id}\` — the maintained O(1) ${grouped ? `total per \`${s.by}\`` : "grand total"}. */`,
|
|
334
|
+
grouped
|
|
335
|
+
? ` ${lcFirst(camel(s.id))}: (${s.by}: string) => holon.sum(${JSON.stringify(s.id)}, ${s.by}),`
|
|
336
|
+
: ` ${lcFirst(camel(s.id))}: () => holon.sum(${JSON.stringify(s.id)}),`,
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
for (const agg of mod.aggregates as AggregateHandle[]) {
|
|
340
|
+
methods.push(
|
|
341
|
+
` /** Read one ${agg.id} by aggregate id. */`,
|
|
342
|
+
` ${lcFirst(camel(agg.id))}ById: (id: string) => holon.queryById(id) as Promise<Row<${agg.id}Data>[]>,`,
|
|
343
|
+
` /** Every ${agg.id} (the type-index probe — O(result), not a scan). */`,
|
|
344
|
+
` list${agg.id}s: () => holon.query("aggregatesByType", { __type: ${JSON.stringify(agg.id)} }) as Promise<Row<${agg.id}Data>[]>,`,
|
|
345
|
+
` /** Watch one ${agg.id} by aggregate id (local reactive read; returns stop()). */`,
|
|
346
|
+
` watch${agg.id}ById: (id: string, cb: (rows: Row<${agg.id}Data>[]) => void, opts?: { intervalMs?: number }) =>`,
|
|
347
|
+
` holon.watchById(id, cb as (rows: unknown) => void, opts),`,
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
out.push(
|
|
352
|
+
`/** Typed client for the \`${domain}\` domain, bound to a connected holon. */`,
|
|
353
|
+
`export function ${lcFirst(camel(domain))}Client(holon: NomosHolon, domainHash: string = ${hashConst}) {`,
|
|
354
|
+
` return {`,
|
|
355
|
+
` domain: ${JSON.stringify(domain)} as const,`,
|
|
356
|
+
` domainHash,`,
|
|
357
|
+
...methods,
|
|
358
|
+
` };`,
|
|
359
|
+
`}`,
|
|
360
|
+
``,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return out.join("\n");
|
|
365
|
+
}
|