@githolon/dsl 0.2.0 → 0.2.2
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 +3 -1
- package/src/codegen_proof.ts +430 -0
- package/src/codegen_ts.ts +8 -6
- package/src/compile_package_main.ts +289 -10
- package/src/engine_entry.ts +236 -83
- package/src/usd_layers.ts +722 -0
- package/src/usd_state.ts +505 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
"./manifest": "./src/manifest.ts",
|
|
20
20
|
"./compose": "./src/compose.ts",
|
|
21
21
|
"./usd": "./src/usd.ts",
|
|
22
|
+
"./usd-layers": "./src/usd_layers.ts",
|
|
23
|
+
"./usd-state": "./src/usd_state.ts",
|
|
22
24
|
"./engine-entry": "./src/engine_entry.ts",
|
|
23
25
|
"./build-package": "./src/build_package.ts",
|
|
24
26
|
"./compile-engine": "./src/compile_engine.ts",
|
|
@@ -0,0 +1,430 @@
|
|
|
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
|
+
* GENERATED PROOF CODEGEN — `build/<name>.proof.mts`, the runnable e2e SYNTHESIZED
|
|
10
|
+
* FROM THE LAW ITSELF (the field-test-6 ask: reshaping the domain meant rewriting
|
|
11
|
+
* test/e2e.mts by hand; now the proof reshapes itself on every compile).
|
|
12
|
+
*
|
|
13
|
+
* `generateTsProof(modules, opts)` reads the domain's ACTUAL directives / queries /
|
|
14
|
+
* counts (the same composed modules every other emitter sees) and emits a
|
|
15
|
+
* domain-shaped live proof:
|
|
16
|
+
*
|
|
17
|
+
* throwaway workspace → deploy `build/<name>.deploy.json` → connect via the
|
|
18
|
+
* GENERATED typed client → fetch-TRAPPED offline dispatch of the first `.creates`
|
|
19
|
+
* directive (payload synthesized from its zod schema) → first declared query +
|
|
20
|
+
* first count answered LOCALLY → sync (edge admission) → the CLOUD answers →
|
|
21
|
+
* two-client AddWins concurrency (when the law has the shape for it) →
|
|
22
|
+
* convergence → ALL GREEN.
|
|
23
|
+
*
|
|
24
|
+
* Payload SYNTHESIS rules ({@link synthesizePayload}): string → `"sample-<field>"`,
|
|
25
|
+
* enum → its first option, number → 1, boolean → true, array of string →
|
|
26
|
+
* `["sample-a"]`, literal → its value, `.optional()`/`.default()` → omitted,
|
|
27
|
+
* required `…At` strings → omitted (the generated client auto-stamps ISO now() at
|
|
28
|
+
* dispatch — the same {@link autoStampFields} convention codegen_ts types by).
|
|
29
|
+
* NO FALLBACK: a shape the synthesizer can't sample throws, naming the field and
|
|
30
|
+
* the remedy — the compile then SKIPS the proof (the law still ships) and says why.
|
|
31
|
+
*
|
|
32
|
+
* Deterministic codegen: the emitted bytes are a pure function of the modules +
|
|
33
|
+
* package name + domain hash — no timestamps, no randomness (the proof picks its
|
|
34
|
+
* throwaway workspace name at RUN time, not at generation time).
|
|
35
|
+
*/
|
|
36
|
+
import type { z } from "zod";
|
|
37
|
+
|
|
38
|
+
import type { AggregateHandle } from "./aggregate.js";
|
|
39
|
+
import type { Field } from "./fields.js";
|
|
40
|
+
import type { Directive } from "./directive.js";
|
|
41
|
+
import type { DomainModule } from "./codegen_dart.js";
|
|
42
|
+
import type { QueryDecl } from "./query.js";
|
|
43
|
+
import { finishCount } from "./count.js";
|
|
44
|
+
import {
|
|
45
|
+
autoStampFields,
|
|
46
|
+
tsClientFactoryName,
|
|
47
|
+
tsHashConstName,
|
|
48
|
+
zodArrayElement,
|
|
49
|
+
zodDef,
|
|
50
|
+
zodEnumValues,
|
|
51
|
+
zodKind,
|
|
52
|
+
zodObjectShape,
|
|
53
|
+
} from "./codegen_ts.js";
|
|
54
|
+
|
|
55
|
+
const camel = (s: string) => s.replace(/[_\s-]+(\w)/g, (_m, c: string) => c.toUpperCase());
|
|
56
|
+
const lcFirst = (s: string) => (s.length ? s[0]!.toLowerCase() + s.slice(1) : s);
|
|
57
|
+
|
|
58
|
+
// ── payload synthesis (the sample-value sibling of codegen_ts's type walker) ──────
|
|
59
|
+
|
|
60
|
+
export interface SynthesizedPayload {
|
|
61
|
+
/** The literal the proof dispatches. Auto-stamped `…At` fields are OMITTED here. */
|
|
62
|
+
readonly value: Record<string, unknown>;
|
|
63
|
+
/** Required `…At` string fields deliberately omitted — the generated client stamps ISO now() at dispatch. */
|
|
64
|
+
readonly autoStamped: readonly string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** One sample value for one zod shape. NO FALLBACK: an unhandled shape throws, naming its remedy. */
|
|
68
|
+
function sampleOfZod(zt: z.ZodTypeAny, field: string, where: string): unknown {
|
|
69
|
+
switch (zodKind(zt)) {
|
|
70
|
+
case "string":
|
|
71
|
+
return `sample-${field}`;
|
|
72
|
+
case "number":
|
|
73
|
+
return 1;
|
|
74
|
+
case "boolean":
|
|
75
|
+
return true;
|
|
76
|
+
case "enum":
|
|
77
|
+
return zodEnumValues(zt)[0];
|
|
78
|
+
case "literal": {
|
|
79
|
+
const def = zodDef(zt);
|
|
80
|
+
return def.value !== undefined ? def.value : def.values?.[0];
|
|
81
|
+
}
|
|
82
|
+
case "array": {
|
|
83
|
+
const el = zodArrayElement(zt);
|
|
84
|
+
return zodKind(el) === "string" ? ["sample-a"] : [sampleOfZod(el, field, where)];
|
|
85
|
+
}
|
|
86
|
+
case "object": {
|
|
87
|
+
const out: Record<string, unknown> = {};
|
|
88
|
+
for (const [name, raw] of Object.entries(zodObjectShape(zt))) {
|
|
89
|
+
const k = zodKind(raw as z.ZodTypeAny);
|
|
90
|
+
if (k === "optional" || k === "default") continue; // optional → omit
|
|
91
|
+
out[name] = sampleOfZod(raw as z.ZodTypeAny, name, where);
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
case "record":
|
|
96
|
+
return {};
|
|
97
|
+
case "nullable":
|
|
98
|
+
case "optional":
|
|
99
|
+
case "default":
|
|
100
|
+
return sampleOfZod(zodDef(zt).innerType!, field, where);
|
|
101
|
+
case "union": {
|
|
102
|
+
const first = zodDef(zt).options?.[0];
|
|
103
|
+
if (first !== undefined) return sampleOfZod(first, field, where);
|
|
104
|
+
throw new Error(`codegen-proof: empty union at field '${field}' of ${where}`);
|
|
105
|
+
}
|
|
106
|
+
default:
|
|
107
|
+
throw new Error(
|
|
108
|
+
`codegen-proof: cannot synthesize a sample for zod shape '${zodKind(zt)}' (field '${field}' of ${where}) — reshape the payload to a sampleable type, or prove that directive by hand in test/e2e.mts`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Synthesize a dispatchable sample payload from a directive's zod object schema:
|
|
115
|
+
* optional/defaulted fields omitted, required `…At` strings omitted (auto-stamped
|
|
116
|
+
* by the generated client), everything else sampled by {@link sampleOfZod}.
|
|
117
|
+
*/
|
|
118
|
+
export function synthesizePayload(schema: z.ZodTypeAny, where: string): SynthesizedPayload {
|
|
119
|
+
if (zodKind(schema) !== "object") {
|
|
120
|
+
throw new Error(`codegen-proof: ${where} payload is not a zod object — the proof can only synthesize object payloads`);
|
|
121
|
+
}
|
|
122
|
+
const autoStamped = autoStampFields(schema);
|
|
123
|
+
const stamped = new Set(autoStamped);
|
|
124
|
+
const value: Record<string, unknown> = {};
|
|
125
|
+
for (const [name, raw] of Object.entries(zodObjectShape(schema))) {
|
|
126
|
+
const f = raw as z.ZodTypeAny;
|
|
127
|
+
const k = zodKind(f);
|
|
128
|
+
if (k === "optional" || k === "default") continue; // optional → omit
|
|
129
|
+
if (stamped.has(name)) continue; // `…At` → omit; the client stamps it
|
|
130
|
+
value[name] = sampleOfZod(f, name, where);
|
|
131
|
+
}
|
|
132
|
+
return { value, autoStamped };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── shape discovery (which legs THIS law can prove) ───────────────────────────────
|
|
136
|
+
|
|
137
|
+
interface ConcurrencyLeg {
|
|
138
|
+
readonly directive: Directive<unknown>;
|
|
139
|
+
/** The payload field that carries the target instance id (first required string `…Id`). */
|
|
140
|
+
readonly idField: string;
|
|
141
|
+
/** The AddWins set field the directive adds to (payload array field name === aggregate set field name). */
|
|
142
|
+
readonly setField: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The AddWins two-writer leg needs the law to have the shape for it: a `.mutates`
|
|
147
|
+
* directive on the created aggregate whose payload carries (a) a required string
|
|
148
|
+
* `…Id` field (the instance) and (b) an array-of-string field named exactly like
|
|
149
|
+
* one of the aggregate's AddWins set fields (the `addToSet` signature). Absent
|
|
150
|
+
* that shape, the proof prints a skip note instead of inventing law.
|
|
151
|
+
*/
|
|
152
|
+
function findConcurrencyLeg(
|
|
153
|
+
directives: readonly Directive<unknown>[],
|
|
154
|
+
agg: AggregateHandle,
|
|
155
|
+
): ConcurrencyLeg | undefined {
|
|
156
|
+
const addWinsSets = Object.entries(agg.fields)
|
|
157
|
+
.filter(([, f]) => (f as Field).kind === "set" && ((f as Field).driver as { kind?: string }).kind === "AddWins")
|
|
158
|
+
.map(([name]) => name);
|
|
159
|
+
if (addWinsSets.length === 0) return undefined;
|
|
160
|
+
for (const d of directives) {
|
|
161
|
+
if (d.marker !== "mutates" || d.aggregateId !== agg.id) continue;
|
|
162
|
+
let shape: Record<string, z.ZodTypeAny>;
|
|
163
|
+
try {
|
|
164
|
+
shape = zodObjectShape(d.payloadSchema as z.ZodTypeAny);
|
|
165
|
+
} catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const idField = Object.entries(shape).find(
|
|
169
|
+
([name, f]) => name.length > 2 && name.endsWith("Id") && zodKind(f) === "string",
|
|
170
|
+
)?.[0];
|
|
171
|
+
if (idField === undefined) continue;
|
|
172
|
+
const setField = addWinsSets.find((s) => {
|
|
173
|
+
const f = shape[s];
|
|
174
|
+
return f !== undefined && zodKind(f) === "array" && zodKind(zodArrayElement(f)) === "string";
|
|
175
|
+
});
|
|
176
|
+
if (setField !== undefined) return { directive: d, idField, setField };
|
|
177
|
+
}
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── the generator ─────────────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
export interface TsProofOptions {
|
|
184
|
+
/** The package name (`nomos.package.mjs` `name`) — names the artifacts the proof loads. */
|
|
185
|
+
readonly packageName: string;
|
|
186
|
+
/** sha256 of the built `.package.usda` — checked against the deploy response. */
|
|
187
|
+
readonly domainHash: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function generateTsProof(modules: readonly DomainModule[], opts: TsProofOptions): string {
|
|
191
|
+
// The proof drives the FIRST domain that has a creating write — without one
|
|
192
|
+
// there is nothing to author, so there is nothing to prove.
|
|
193
|
+
const mod = modules.find((m) =>
|
|
194
|
+
(m.directives as Directive<unknown>[]).some((d) => d?.marker === "creates" && typeof d.payloadSchema === "object"),
|
|
195
|
+
);
|
|
196
|
+
if (mod === undefined) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`no .creates directive in any domain — a generated proof needs one creating write; add a directive(...).creates(...) (test/e2e.mts remains the hand-authored lane)`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
const domain = mod.domain ?? mod.name;
|
|
202
|
+
const factory = tsClientFactoryName(domain);
|
|
203
|
+
const hashConst = tsHashConstName(opts.packageName);
|
|
204
|
+
|
|
205
|
+
const createDir = (mod.directives as Directive<unknown>[]).find((d) => d?.marker === "creates")!;
|
|
206
|
+
const agg = (mod.aggregates as AggregateHandle[]).find((a) => a.id === createDir.aggregateId);
|
|
207
|
+
if (agg === undefined) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
`directive '${createDir.id}' creates '${createDir.aggregateId}' but no such aggregate is composed — export the aggregate from a listed module`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
const synth = synthesizePayload(createDir.payloadSchema as z.ZodTypeAny, `${domain}/${createDir.id}`);
|
|
213
|
+
|
|
214
|
+
// The first declared query that can READ BACK the create: returns the created
|
|
215
|
+
// aggregate and every key field has a synthesized payload value to probe with.
|
|
216
|
+
const query = ((mod.queries ?? []) as QueryDecl[]).find(
|
|
217
|
+
(q) => q.returns === agg.id && q.key.length > 0 && q.key.every((k) => !k.includes(".") && synth.value[k] !== undefined),
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
// The first declared count the create moves: counts the created aggregate,
|
|
221
|
+
// unpredicated (a .where can't be evaluated at codegen time), and — when
|
|
222
|
+
// grouped — grouped by a field the synthesized payload carries.
|
|
223
|
+
const count = (mod.counts ?? [])
|
|
224
|
+
.map((raw) => finishCount(raw))
|
|
225
|
+
.find(
|
|
226
|
+
(c) =>
|
|
227
|
+
c.of === agg.id &&
|
|
228
|
+
(c as { where?: unknown }).where == null &&
|
|
229
|
+
(c.by == null || synth.value[c.by] !== undefined),
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const concurrency = findConcurrencyLeg(mod.directives as Directive<unknown>[], agg);
|
|
233
|
+
|
|
234
|
+
// Workspace-name slug: any package name → a safe, recognizable ws prefix.
|
|
235
|
+
const slug =
|
|
236
|
+
opts.packageName
|
|
237
|
+
.toLowerCase()
|
|
238
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
239
|
+
.replace(/^-+|-+$/g, "")
|
|
240
|
+
.slice(0, 24) || "domain";
|
|
241
|
+
|
|
242
|
+
const payloadLit = JSON.stringify(synth.value);
|
|
243
|
+
const queryParams = query ? JSON.stringify(Object.fromEntries(query.key.map((k) => [k, synth.value[k]]))) : "";
|
|
244
|
+
const listAccessor = `list${agg.id}s`;
|
|
245
|
+
|
|
246
|
+
let step = 0;
|
|
247
|
+
const n = () => ++step;
|
|
248
|
+
const out: string[] = [];
|
|
249
|
+
|
|
250
|
+
out.push(
|
|
251
|
+
`// AUTO-GENERATED by nomos-compile — the RUNNABLE PROOF for package "${opts.packageName}",`,
|
|
252
|
+
`// synthesized from the domain's OWN directives/queries/counts. Reshape the law,`,
|
|
253
|
+
`// recompile, and this proof reshapes itself — no test rewriting. Run it:`,
|
|
254
|
+
`//`,
|
|
255
|
+
`// npx githolon proof (or: npx tsx build/${opts.packageName}.proof.mts)`,
|
|
256
|
+
`//`,
|
|
257
|
+
`// The flow mirrors the scaffold's test/e2e.mts (docs/ in a scaffolded app explains`,
|
|
258
|
+
`// every call — start at docs/01-mental-model.md). Regenerated on every compile; do not edit.`,
|
|
259
|
+
`import { readFileSync } from "node:fs";`,
|
|
260
|
+
`import { connect } from "@githolon/client";`,
|
|
261
|
+
`import { ${factory}, ${hashConst} } from "./${opts.packageName}.client.ts";`,
|
|
262
|
+
``,
|
|
263
|
+
`const CLOUD = (process.env.NOMOS_CLOUD || "https://nomos.captainapp.co.uk").replace(/\\/+$/, "");`,
|
|
264
|
+
`const WS = process.env.NOMOS_WS || ${JSON.stringify(slug + "-proof-")} + Math.random().toString(36).slice(2, 8);`,
|
|
265
|
+
``,
|
|
266
|
+
`// IDENTITY NOTE: this proof uses the bare x-nomos-principal lane so it is`,
|
|
267
|
+
`// SELF-CONTAINED (a throwaway workspace, no stored credentials). Real apps use`,
|
|
268
|
+
`// \`githolon login --agent\` (a VERIFIED identity) + \`githolon ws create/deploy\`.`,
|
|
269
|
+
`const fail: (m: string) => never = (m) => { console.error("✗ " + m); process.exit(1); };`,
|
|
270
|
+
`const ok = (m: string) => console.log("✓ " + m);`,
|
|
271
|
+
``,
|
|
272
|
+
`const deploy = JSON.parse(readFileSync(new URL(${JSON.stringify(`./${opts.packageName}.deploy.json`)}, import.meta.url), "utf8"));`,
|
|
273
|
+
``,
|
|
274
|
+
`// ${n()}. a throwaway workspace (the ONE-TIME secret comes back on create)`,
|
|
275
|
+
`let r = await fetch(\`\${CLOUD}/v1/workspaces/\${WS}\`, { method: "POST", headers: { "x-nomos-principal": "githolon-proof" } });`,
|
|
276
|
+
`let d = await r.json();`,
|
|
277
|
+
`if (!d.ok) fail(\`create workspace: \${JSON.stringify(d)}\`);`,
|
|
278
|
+
`const SECRET: string = d.workspaceSecret;`,
|
|
279
|
+
`if (!SECRET?.startsWith("nws_v1_")) fail(\`no workspaceSecret returned: \${JSON.stringify(d)}\`);`,
|
|
280
|
+
`ok(\`workspace \${WS} created — secret \${SECRET.slice(0, 10)}…\`);`,
|
|
281
|
+
``,
|
|
282
|
+
`// ${n()}. deploy YOUR compiled law (build/${opts.packageName}.deploy.json) WITH the secret`,
|
|
283
|
+
`r = await fetch(\`\${CLOUD}/v1/workspaces/\${WS}/domains\`, { method: "POST", headers: { "content-type": "application/json", authorization: \`Bearer \${SECRET}\` }, body: JSON.stringify(deploy) });`,
|
|
284
|
+
`d = await r.json();`,
|
|
285
|
+
`if (!d.ok || d.installation?.[0]?.data?.["status.phase"] !== "Active") fail(\`deploy: \${JSON.stringify(d)}\`);`,
|
|
286
|
+
`if (d.domainHash !== ${hashConst}) fail(\`deploy hash \${d.domainHash} != the generated client's \${${hashConst}}\`);`,
|
|
287
|
+
`ok(\`law deployed Active — \${${hashConst}.slice(0, 12)}… (the content hash baked into your typed client)\`);`,
|
|
288
|
+
``,
|
|
289
|
+
`// ${n()}. connect + bind the GENERATED typed client`,
|
|
290
|
+
`const holon = await connect({ cloud: CLOUD, workspace: WS, clientId: "proof-1" });`,
|
|
291
|
+
`const app = ${factory}(holon);`,
|
|
292
|
+
`ok(\`web client connected — ${factory} bound (replica \${holon.replica.slice(0, 6)}…)\`);`,
|
|
293
|
+
``,
|
|
294
|
+
`// ${n()}. OFFLINE write — the proof is ENFORCED, not narrated: while we author and`,
|
|
295
|
+
`// read LOCALLY, fetch THROWS. The payload below was SYNTHESIZED from the zod`,
|
|
296
|
+
`// schema of \`${domain}/${createDir.id}\`${synth.autoStamped.length ? ` (${synth.autoStamped.map((s) => `\`${s}\``).join("/")} omitted — the client auto-stamps ISO now())` : ``}.`,
|
|
297
|
+
`const realFetch = globalThis.fetch;`,
|
|
298
|
+
`globalThis.fetch = (() => { throw new Error("OFFLINE PROOF VIOLATED: local authoring touched the network"); }) as typeof fetch;`,
|
|
299
|
+
``,
|
|
300
|
+
`await app.${createDir.id}(${payloadLit});`,
|
|
301
|
+
`ok("dispatch ${domain}/${createDir.id} — offline write under the pulled law (payload synthesized from YOUR schema)");`,
|
|
302
|
+
``,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
if (query) {
|
|
306
|
+
out.push(
|
|
307
|
+
`// ${n()}. the first declared query routes LOCALLY (params from the synthesized payload)`,
|
|
308
|
+
`const rows = await app.${lcFirst(camel(query.id))}(${queryParams});`,
|
|
309
|
+
`if (rows.length !== 1) fail(\`local declared query ${query.id}: \${JSON.stringify(rows)}\`);`,
|
|
310
|
+
`const createdId = rows[0]!.id;`,
|
|
311
|
+
`ok(\`declared query ${query.id} answers locally — ${agg.id} \${createdId}\`);`,
|
|
312
|
+
``,
|
|
313
|
+
);
|
|
314
|
+
} else {
|
|
315
|
+
out.push(
|
|
316
|
+
`// ${n()}. no declared query reads this create back (declare a query(...) keyed on a`,
|
|
317
|
+
`// payload field to see one proved here) — the type-index probe captures the id.`,
|
|
318
|
+
`const rows = await app.${listAccessor}();`,
|
|
319
|
+
`if (rows.length !== 1) fail(\`local ${listAccessor}: \${JSON.stringify(rows)}\`);`,
|
|
320
|
+
`const createdId = rows[0]!.id;`,
|
|
321
|
+
`console.log("– declared-query leg SKIPPED — no query(...) returns ${agg.id} keyed on a synthesized payload field");`,
|
|
322
|
+
`ok(\`local read answers — ${agg.id} \${createdId}\`);`,
|
|
323
|
+
``,
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (count) {
|
|
328
|
+
const accessor = lcFirst(camel(count.id));
|
|
329
|
+
const callArg = count.by != null ? JSON.stringify(String(synth.value[count.by])) : "";
|
|
330
|
+
out.push(
|
|
331
|
+
`// ${n()}. the first declared count — the maintained O(1) tally, locally`,
|
|
332
|
+
`const localCount = await app.${accessor}(${callArg});`,
|
|
333
|
+
`if (localCount !== 1) fail(\`local declared count ${count.id}: \${localCount}\`);`,
|
|
334
|
+
`ok("declared count ${count.id}${count.by != null ? `(${callArg.replace(/"/g, "'")})` : `()`} = 1 locally");`,
|
|
335
|
+
``,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
out.push(
|
|
340
|
+
`globalThis.fetch = realFetch;`,
|
|
341
|
+
`ok("offline proof ENFORCED — fetch was trapped: local write/read touched no network");`,
|
|
342
|
+
``,
|
|
343
|
+
`// ${n()}. sync: push the session branch; edge admission re-verifies and merges to main`,
|
|
344
|
+
`const s = await holon.sync();`,
|
|
345
|
+
`if (!s.admission?.ok) fail(\`sync/admit: \${JSON.stringify(s)}\`);`,
|
|
346
|
+
`const admitted = (s.admission.sessions || []).flatMap((x: { admitted?: unknown[] }) => x.admitted || []);`,
|
|
347
|
+
`if (admitted.length < 1) fail(\`edge admitted \${admitted.length} intent(s): \${JSON.stringify(s.admission)}\`);`,
|
|
348
|
+
`ok(\`synced — edge admission merged \${admitted.length} intent(s) to main\`);`,
|
|
349
|
+
``,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
if (query) {
|
|
353
|
+
out.push(
|
|
354
|
+
`// ${n()}. the CLOUD answers the SAME declared query (read-manifest overlay, edge projection)`,
|
|
355
|
+
`d = await (await fetch(\`\${CLOUD}/v1/workspaces/\${WS}/query?queryId=${query.id}¶ms=\${encodeURIComponent(${JSON.stringify(queryParams)})}\`)).json();`,
|
|
356
|
+
`if (!(d.ok && d.rows.length === 1 && d.rows[0].id === createdId)) fail(\`cloud declared query: \${JSON.stringify(d)}\`);`,
|
|
357
|
+
`ok("cloud declared query ${query.id} returns the admitted row — full loop closed");`,
|
|
358
|
+
``,
|
|
359
|
+
);
|
|
360
|
+
} else {
|
|
361
|
+
out.push(
|
|
362
|
+
`// ${n()}. the CLOUD answers the by-id read (edge projection of the admitted write)`,
|
|
363
|
+
`d = await (await fetch(\`\${CLOUD}/v1/workspaces/\${WS}/aggregates/\${encodeURIComponent(createdId)}\`)).json();`,
|
|
364
|
+
`if (!(d.rows?.length === 1 && d.rows[0].id === createdId)) fail(\`cloud by-id read: \${JSON.stringify(d)}\`);`,
|
|
365
|
+
`ok("cloud read returns the admitted row — full loop closed");`,
|
|
366
|
+
``,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (count) {
|
|
371
|
+
out.push(
|
|
372
|
+
`// ${n()}. the EDGE maintains the same count (same wasm, same law)`,
|
|
373
|
+
`d = await (await fetch(\`\${CLOUD}/v1/workspaces/\${WS}/counts/${count.id}${count.by != null ? `?group=\${encodeURIComponent(${JSON.stringify(String(synth.value[count.by]))})}` : ``}\`)).json();`,
|
|
374
|
+
`if (!(d.ok && d.count === 1)) fail(\`cloud count ${count.id}: \${JSON.stringify(d)}\`);`,
|
|
375
|
+
`ok("cloud count ${count.id} = 1 — the O(1) maintained read, at the edge too");`,
|
|
376
|
+
``,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (concurrency) {
|
|
381
|
+
const { directive: cd, idField, setField } = concurrency;
|
|
382
|
+
const cSynth = synthesizePayload(cd.payloadSchema as z.ZodTypeAny, `${domain}/${cd.id}`);
|
|
383
|
+
const concPayload = (writer: string): string => {
|
|
384
|
+
const entries = Object.entries({ ...cSynth.value, [idField]: 0, [setField]: 0 }).map(([k]) => {
|
|
385
|
+
if (k === idField) return `${JSON.stringify(k)}: createdId`;
|
|
386
|
+
if (k === setField) return `${JSON.stringify(k)}: ${JSON.stringify([`proof-${writer}`])}`;
|
|
387
|
+
return `${JSON.stringify(k)}: ${JSON.stringify(cSynth.value[k])}`;
|
|
388
|
+
});
|
|
389
|
+
return `{ ${entries.join(", ")} }`;
|
|
390
|
+
};
|
|
391
|
+
out.push(
|
|
392
|
+
`// ${n()}. CONCURRENCY — two writers, two clientIds, BLIND to each other: each adds to`,
|
|
393
|
+
`// the AddWins set \`${agg.id}.${setField}\` via \`${domain}/${cd.id}\` offline; admission`,
|
|
394
|
+
`// merges both and the union keeps every add.`,
|
|
395
|
+
`const writerA = await connect({ cloud: CLOUD, workspace: WS, clientId: "proof-a" });`,
|
|
396
|
+
`const writerB = await connect({ cloud: CLOUD, workspace: WS, clientId: "proof-b" });`,
|
|
397
|
+
`await ${factory}(writerA).${cd.id}(${concPayload("a")});`,
|
|
398
|
+
`await ${factory}(writerB).${cd.id}(${concPayload("b")});`,
|
|
399
|
+
`await writerA.sync();`,
|
|
400
|
+
`await writerB.sync();`,
|
|
401
|
+
`d = await (await fetch(\`\${CLOUD}/v1/workspaces/\${WS}/aggregates/\${encodeURIComponent(createdId)}\`)).json();`,
|
|
402
|
+
`const merged: string[] = d.rows?.[0]?.data?.[${JSON.stringify(setField)}] ?? [];`,
|
|
403
|
+
`if (!(merged.includes("proof-a") && merged.includes("proof-b"))) fail(\`AddWins union lost an add: \${JSON.stringify(merged)}\`);`,
|
|
404
|
+
`ok(\`two blind writers' concurrent adds BOTH survive the merge — ${setField} [\${[...merged].sort().join(", ")}]\`);`,
|
|
405
|
+
``,
|
|
406
|
+
);
|
|
407
|
+
} else {
|
|
408
|
+
out.push(
|
|
409
|
+
`// ${n()}. concurrency leg: this law has no addToSet-capable .mutates directive on an`,
|
|
410
|
+
`// AddWins set field, so there is nothing honest to race.`,
|
|
411
|
+
`console.log("– concurrency leg SKIPPED — add a .mutates directive carrying an array field named like an AddWins set (the addToSet shape) to see two blind writers merge");`,
|
|
412
|
+
``,
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
out.push(
|
|
417
|
+
`// ${n()}. CONVERGENCE: the original instance adopts canonical main in place — no reconnect`,
|
|
418
|
+
`await holon.pull();`,
|
|
419
|
+
`const head = await holon.head();`,
|
|
420
|
+
`const refsAdv = await (await fetch(\`\${CLOUD}/v1/workspaces/\${WS}/git/info/refs?service=git-upload-pack\`)).text();`,
|
|
421
|
+
`const remoteMain = (refsAdv.match(/([0-9a-f]{40}) refs\\/heads\\/main/) || [])[1];`,
|
|
422
|
+
`if (head !== remoteMain) fail(\`not converged in place: local \${head?.slice(0, 10)} vs main \${remoteMain?.slice(0, 10)}\`);`,
|
|
423
|
+
`ok("converged in place — local head == canonical main");`,
|
|
424
|
+
``,
|
|
425
|
+
`console.log(\`\\nALL GREEN — generated proof for "${opts.packageName}": deploy → offline write → local reads → admission → cloud reads${concurrency ? ` → AddWins merge` : ``} → convergence (\${WS})\`);`,
|
|
426
|
+
``,
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
return out.join("\n");
|
|
430
|
+
}
|
package/src/codegen_ts.ts
CHANGED
|
@@ -63,6 +63,8 @@ export const tsClientFactoryName = (domain: string): string => `${lcFirst(camel(
|
|
|
63
63
|
export const tsHashConstName = (packageName: string): string => `${upperSnake(packageName)}_DOMAIN_HASH`;
|
|
64
64
|
|
|
65
65
|
// ── zod → TS (the same `_def ?? def` convention as codegen_dart's walker) ─────────
|
|
66
|
+
// The walkers are exported: codegen_proof.ts synthesizes SAMPLE payloads from the
|
|
67
|
+
// same zod shapes this file derives TYPES from — one introspection, two emitters.
|
|
66
68
|
|
|
67
69
|
type ZodInternalDef = {
|
|
68
70
|
type?: string | z.ZodTypeAny;
|
|
@@ -76,17 +78,17 @@ type ZodInternalDef = {
|
|
|
76
78
|
valueType?: z.ZodTypeAny;
|
|
77
79
|
};
|
|
78
80
|
|
|
79
|
-
function zodDef(zt: z.ZodTypeAny): ZodInternalDef {
|
|
81
|
+
export function zodDef(zt: z.ZodTypeAny): ZodInternalDef {
|
|
80
82
|
const raw = zt as unknown as { _def?: ZodInternalDef; def?: ZodInternalDef };
|
|
81
83
|
return raw._def ?? raw.def ?? {};
|
|
82
84
|
}
|
|
83
85
|
|
|
84
|
-
function zodKind(zt: z.ZodTypeAny): string {
|
|
86
|
+
export function zodKind(zt: z.ZodTypeAny): string {
|
|
85
87
|
const t = zodDef(zt).type;
|
|
86
88
|
return typeof t === "string" ? t : "unknown";
|
|
87
89
|
}
|
|
88
90
|
|
|
89
|
-
function zodEnumValues(zt: z.ZodTypeAny): string[] {
|
|
91
|
+
export function zodEnumValues(zt: z.ZodTypeAny): string[] {
|
|
90
92
|
const raw = zt as unknown as { options?: readonly unknown[] };
|
|
91
93
|
if (raw.options !== undefined) return raw.options.map(String);
|
|
92
94
|
const entries = zodDef(zt).entries;
|
|
@@ -94,7 +96,7 @@ function zodEnumValues(zt: z.ZodTypeAny): string[] {
|
|
|
94
96
|
throw new Error("codegen-ts: ZodEnum has no values");
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
function zodObjectShape(zt: z.ZodTypeAny): Record<string, z.ZodTypeAny> {
|
|
99
|
+
export function zodObjectShape(zt: z.ZodTypeAny): Record<string, z.ZodTypeAny> {
|
|
98
100
|
const shape = zodDef(zt).shape;
|
|
99
101
|
if (typeof shape === "function") return shape();
|
|
100
102
|
if (shape !== undefined) return shape;
|
|
@@ -103,7 +105,7 @@ function zodObjectShape(zt: z.ZodTypeAny): Record<string, z.ZodTypeAny> {
|
|
|
103
105
|
throw new Error("codegen-ts: ZodObject has no shape");
|
|
104
106
|
}
|
|
105
107
|
|
|
106
|
-
function zodArrayElement(zt: z.ZodTypeAny): z.ZodTypeAny {
|
|
108
|
+
export function zodArrayElement(zt: z.ZodTypeAny): z.ZodTypeAny {
|
|
107
109
|
const def = zodDef(zt);
|
|
108
110
|
if (def.element !== undefined) return def.element;
|
|
109
111
|
if (typeof def.type !== "string" && def.type !== undefined) return def.type;
|
|
@@ -175,7 +177,7 @@ function tsTypeOfZod(zt: z.ZodTypeAny, indent: string): string {
|
|
|
175
177
|
* Passing the field explicitly always wins (scheduling-shaped fields like
|
|
176
178
|
* `expiresAt` should be passed explicitly — "now" is only the DEFAULT).
|
|
177
179
|
*/
|
|
178
|
-
function autoStampFields(schema: z.ZodTypeAny): string[] {
|
|
180
|
+
export function autoStampFields(schema: z.ZodTypeAny): string[] {
|
|
179
181
|
if (zodKind(schema) !== "object") return [];
|
|
180
182
|
const out: string[] = [];
|
|
181
183
|
for (const [name, f] of Object.entries(zodObjectShape(schema))) {
|