@githolon/dsl 0.1.3 → 0.2.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 +9 -0
- package/package.json +1 -1
- package/src/codegen_ts.ts +85 -9
- package/src/compile_package_main.ts +12 -6
- package/src/index.ts +3 -0
package/LICENSE.md
CHANGED
|
@@ -24,6 +24,15 @@ with it; we keep the rest for now.
|
|
|
24
24
|
- offer the Nomos runtime, or anything materially similar, as a hosted service;
|
|
25
25
|
- reverse-engineer the holon wasm runtime.
|
|
26
26
|
|
|
27
|
+
## Data retention
|
|
28
|
+
|
|
29
|
+
This is a pre-release we are actively evaluating. Workspaces you retire stop
|
|
30
|
+
counting toward your quota but are NOT deleted: **we retain all workspace data
|
|
31
|
+
(ledgers, law, intents) and may examine it to evaluate how the product is
|
|
32
|
+
used.** Don't put anything in a pre-release workspace you wouldn't want the
|
|
33
|
+
builders to read. If you need something truly expunged, ask:
|
|
34
|
+
jack@captainapp.co.uk.
|
|
35
|
+
|
|
27
36
|
## The rest
|
|
28
37
|
|
|
29
38
|
Provided **as is**, with no warranty of any kind; to the maximum extent
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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_ts.ts
CHANGED
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
* * a PAYLOAD TYPE per directive — derived from the SAME zod schema the sealed
|
|
19
19
|
* engine validates (recursive zod→TS: enums become literal unions, `.optional()`
|
|
20
20
|
* becomes `?`, `.int()` stays `number`), so a payload the type system accepts is
|
|
21
|
-
* a payload the engine accepts;
|
|
21
|
+
* a payload the engine accepts; required string fields named `…At` are
|
|
22
|
+
* CALLER-STAMPED TIMESTAMPS — optional in the type, auto-filled with ISO now()
|
|
23
|
+
* at dispatch (see {@link autoStampFields});
|
|
22
24
|
* * a READ-MODEL interface per aggregate — derived from the DSL `Field` kinds the
|
|
23
25
|
* projection decodes by (set→`T[]`, map→`Record<string, V>` via `mapValueKind`,
|
|
24
26
|
* `t.jsonObject()`→`Record<string, unknown>`, plain `t.json()`→`unknown`), with
|
|
@@ -45,10 +47,21 @@ import type { DerivedDecl } from "./derived.js";
|
|
|
45
47
|
import type { CombinedDecl } from "./combined.js";
|
|
46
48
|
|
|
47
49
|
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());
|
|
50
|
+
const camel = (s: string) => s.replace(/[_\s-]+(\w)/g, (_m, c: string) => c.toUpperCase());
|
|
49
51
|
const lcFirst = (s: string) => (s.length ? s[0]!.toLowerCase() + s.slice(1) : s);
|
|
50
52
|
const upperSnake = (s: string) => s.replace(/[-\s]+/g, "_").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
|
|
51
53
|
|
|
54
|
+
// ── NAME NORMALIZATION (no naming convention to learn) ────────────────────────────
|
|
55
|
+
// `nomos.package.mjs` names and domain keys may be ANY string — kebab-case,
|
|
56
|
+
// snake_case, space-separated, PascalCase. Artifact FILENAMES keep the raw name
|
|
57
|
+
// verbatim; generated SYMBOLS normalize it:
|
|
58
|
+
|
|
59
|
+
/** `tool-library` / `tool_library` / `Tool Library` → `toolLibraryClient` — the factory symbol. */
|
|
60
|
+
export const tsClientFactoryName = (domain: string): string => `${lcFirst(camel(domain))}Client`;
|
|
61
|
+
|
|
62
|
+
/** `tool-library` / `tool_library` / `Tool Library` → `TOOL_LIBRARY_DOMAIN_HASH` — the hash const. */
|
|
63
|
+
export const tsHashConstName = (packageName: string): string => `${upperSnake(packageName)}_DOMAIN_HASH`;
|
|
64
|
+
|
|
52
65
|
// ── zod → TS (the same `_def ?? def` convention as codegen_dart's walker) ─────────
|
|
53
66
|
|
|
54
67
|
type ZodInternalDef = {
|
|
@@ -151,6 +164,44 @@ function tsTypeOfZod(zt: z.ZodTypeAny, indent: string): string {
|
|
|
151
164
|
}
|
|
152
165
|
}
|
|
153
166
|
|
|
167
|
+
// ── caller-side clock sugar (the `…At` payload convention) ────────────────────────
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Top-level REQUIRED string payload fields named `…At` (`signedAt`, `updatedAt`,
|
|
171
|
+
* `stampedAt`, …) are CALLER-STAMPED TIMESTAMPS by convention: the generated client
|
|
172
|
+
* fills an omitted one with `new Date().toISOString()` at dispatch. The stamp rides
|
|
173
|
+
* IN the payload — the plan stays a pure function of `(payload, ports)` and replays
|
|
174
|
+
* byte-identical on every peer — so the domain dev stops thinking about clocks.
|
|
175
|
+
* Passing the field explicitly always wins (scheduling-shaped fields like
|
|
176
|
+
* `expiresAt` should be passed explicitly — "now" is only the DEFAULT).
|
|
177
|
+
*/
|
|
178
|
+
function autoStampFields(schema: z.ZodTypeAny): string[] {
|
|
179
|
+
if (zodKind(schema) !== "object") return [];
|
|
180
|
+
const out: string[] = [];
|
|
181
|
+
for (const [name, f] of Object.entries(zodObjectShape(schema))) {
|
|
182
|
+
// required-only: `.optional()`/`.default()` wrappers change the zod kind, so they skip.
|
|
183
|
+
if (name.length > 2 && name.endsWith("At") && zodKind(f as z.ZodTypeAny) === "string") out.push(name);
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The payload type text, with auto-stamped fields surfaced as optional (`?`). */
|
|
189
|
+
function tsTypeOfPayload(schema: z.ZodTypeAny, autoStamped: readonly string[]): string {
|
|
190
|
+
if (zodKind(schema) !== "object" || autoStamped.length === 0) return tsTypeOfZod(schema, "");
|
|
191
|
+
const stamped = new Set(autoStamped);
|
|
192
|
+
const fields = Object.entries(zodObjectShape(schema)).map(([name, raw]) => {
|
|
193
|
+
let f = raw as z.ZodTypeAny;
|
|
194
|
+
let optional = stamped.has(name);
|
|
195
|
+
while (zodKind(f) === "optional" || zodKind(f) === "default") {
|
|
196
|
+
optional = true;
|
|
197
|
+
f = zodDef(f).innerType!;
|
|
198
|
+
}
|
|
199
|
+
const note = stamped.has(name) ? " // auto-stamped with ISO now() at dispatch when omitted" : "";
|
|
200
|
+
return ` ${name}${optional ? "?" : ""}: ${tsTypeOfZod(f, " ")};${note}`;
|
|
201
|
+
});
|
|
202
|
+
return fields.length ? `{\n${fields.join("\n")}\n}` : "Record<string, never>";
|
|
203
|
+
}
|
|
204
|
+
|
|
154
205
|
// ── DSL Field → read-model TS type (the projection's decode surface) ──────────────
|
|
155
206
|
|
|
156
207
|
function tsTypeOfField(field: Field): string {
|
|
@@ -214,7 +265,7 @@ export interface TsClientOptions {
|
|
|
214
265
|
}
|
|
215
266
|
|
|
216
267
|
export function generateTsClient(modules: readonly DomainModule[], opts: TsClientOptions): string {
|
|
217
|
-
const hashConst =
|
|
268
|
+
const hashConst = tsHashConstName(opts.packageName);
|
|
218
269
|
const out: string[] = [];
|
|
219
270
|
out.push(
|
|
220
271
|
`// AUTO-GENERATED by nomos-compile — typed TS client for package "${opts.packageName}".`,
|
|
@@ -222,9 +273,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
222
273
|
`// exposing the @githolon/client surface (the structural NomosHolon below), e.g.`,
|
|
223
274
|
`//`,
|
|
224
275
|
`// import { connect } from "@githolon/client";`,
|
|
225
|
-
`// import { ${
|
|
276
|
+
`// import { ${tsClientFactoryName(modules[0]?.domain ?? "domain")} } from "./build/${opts.packageName}.client.js";`,
|
|
226
277
|
`// const holon = await connect({ cloud, workspace, clientId });`,
|
|
227
|
-
`// const app = ${
|
|
278
|
+
`// const app = ${tsClientFactoryName(modules[0]?.domain ?? "domain")}(holon);`,
|
|
228
279
|
``,
|
|
229
280
|
`/** sha256 of the deployed \`.package.usda\` — the installed law this client dispatches under. */`,
|
|
230
281
|
`export const ${hashConst} = ${JSON.stringify(opts.domainHash)};`,
|
|
@@ -250,6 +301,28 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
250
301
|
``,
|
|
251
302
|
);
|
|
252
303
|
|
|
304
|
+
const anyAutoStamp = modules.some((mod) =>
|
|
305
|
+
(mod.directives as Directive<unknown>[]).some(
|
|
306
|
+
(d) =>
|
|
307
|
+
d &&
|
|
308
|
+
typeof (d as { payloadSchema?: unknown }).payloadSchema === "object" &&
|
|
309
|
+
autoStampFields((d as unknown as { payloadSchema: z.ZodTypeAny }).payloadSchema).length > 0,
|
|
310
|
+
),
|
|
311
|
+
);
|
|
312
|
+
if (anyAutoStamp) {
|
|
313
|
+
out.push(
|
|
314
|
+
`/** Caller-side clock sugar: fill omitted \`…At\` timestamp payload fields with ISO now().`,
|
|
315
|
+
` * The stamp rides IN the payload, so plans stay pure and replay byte-identical. */`,
|
|
316
|
+
`const stampNow = <T extends object>(payload: T, fields: string[]): T => {`,
|
|
317
|
+
` const now = new Date().toISOString();`,
|
|
318
|
+
` const out = { ...payload } as Record<string, unknown>;`,
|
|
319
|
+
` for (const f of fields) if (out[f] === undefined) out[f] = now;`,
|
|
320
|
+
` return out as T;`,
|
|
321
|
+
`};`,
|
|
322
|
+
``,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
253
326
|
// ── read models: one interface per aggregate wire type (deduped), deriveds/combineds merged ──
|
|
254
327
|
const aggByType = new Map<string, AggregateHandle>();
|
|
255
328
|
const extrasByType = new Map<string, string[]>();
|
|
@@ -291,14 +364,17 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
291
364
|
if (usedPayloadNames.has(payloadName)) payloadName = `${cap(camel(domain))}${cap(d.id)}Payload`;
|
|
292
365
|
usedPayloadNames.add(payloadName);
|
|
293
366
|
const schema = (d as unknown as { payloadSchema: z.ZodTypeAny }).payloadSchema;
|
|
367
|
+
const stamps = autoStampFields(schema);
|
|
294
368
|
out.push(
|
|
295
|
-
`/** \`${domain}/${d.id}\` payload (mirrors the engine's zod schema). */`,
|
|
296
|
-
`export type ${payloadName} = ${
|
|
369
|
+
`/** \`${domain}/${d.id}\` payload (mirrors the engine's zod schema).${stamps.length ? ` Omitted ${stamps.map((s) => `\`${s}\``).join("/")} auto-stamp(s) ISO now() at dispatch.` : ""} */`,
|
|
370
|
+
`export type ${payloadName} = ${tsTypeOfPayload(schema, stamps)};`,
|
|
297
371
|
``,
|
|
298
372
|
);
|
|
299
373
|
methods.push(
|
|
300
374
|
` /** Author \`${domain}/${d.id}\` locally under the installed law; returns the new local head. */`,
|
|
301
|
-
|
|
375
|
+
stamps.length
|
|
376
|
+
? ` ${d.id}: (payload: ${payloadName}) => holon.dispatch(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, stampNow(payload, ${JSON.stringify(stamps)}), { domainHash }),`
|
|
377
|
+
: ` ${d.id}: (payload: ${payloadName}) => holon.dispatch(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, payload, { domainHash }),`,
|
|
302
378
|
);
|
|
303
379
|
}
|
|
304
380
|
|
|
@@ -350,7 +426,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
350
426
|
|
|
351
427
|
out.push(
|
|
352
428
|
`/** Typed client for the \`${domain}\` domain, bound to a connected holon. */`,
|
|
353
|
-
`export function ${
|
|
429
|
+
`export function ${tsClientFactoryName(domain)}(holon: NomosHolon, domainHash: string = ${hashConst}) {`,
|
|
354
430
|
` return {`,
|
|
355
431
|
` domain: ${JSON.stringify(domain)} as const,`,
|
|
356
432
|
` domainHash,`,
|
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
writeIdentity,
|
|
70
70
|
type Mod,
|
|
71
71
|
} from "./build_package.js";
|
|
72
|
-
import { generateTsClient } from "./codegen_ts.js";
|
|
72
|
+
import { generateTsClient, tsClientFactoryName } from "./codegen_ts.js";
|
|
73
73
|
|
|
74
74
|
const DSL_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
75
75
|
|
|
@@ -288,7 +288,12 @@ async function main(): Promise<void> {
|
|
|
288
288
|
if (!cfg || typeof cfg.name !== "string" || !Array.isArray(cfg.domains) || cfg.domains.length === 0) {
|
|
289
289
|
fail(`config must default-export { name, domains: [{ key, modules }, ...] }`);
|
|
290
290
|
}
|
|
291
|
-
|
|
291
|
+
// ANY readable name works ("tool-library", "tool_library", "Tool Library"):
|
|
292
|
+
// artifact FILENAMES keep it verbatim; generated SYMBOLS normalize it
|
|
293
|
+
// (toolLibraryClient, TOOL_LIBRARY_DOMAIN_HASH — see codegen_ts.ts).
|
|
294
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9 ._-]*$/.test(cfg.name)) {
|
|
295
|
+
fail(`package name '${cfg.name}' is not a safe basename (letters, digits, spaces, '.', '-', '_')`);
|
|
296
|
+
}
|
|
292
297
|
|
|
293
298
|
const outDir = path.resolve(cfgDir, cfg.outDir ?? "build");
|
|
294
299
|
mkdirSync(outDir, { recursive: true });
|
|
@@ -477,7 +482,7 @@ async function main(): Promise<void> {
|
|
|
477
482
|
` queries: ${d.queries.join(", ") || "—"} counts: ${d.counts.join(", ") || "—"}`,
|
|
478
483
|
]),
|
|
479
484
|
`deploy: POST ${cfg.name}.deploy.json to /v1/workspaces/<ws>/domains (or: githolon deploy <ws>)`,
|
|
480
|
-
`typed client: ${cfg.name}.client.ts — ${cfg.name}
|
|
485
|
+
`typed client: ${cfg.name}.client.ts — ${tsClientFactoryName(domainModules[0]?.domain ?? domainModules[0]?.name ?? cfg.name)}(holon), law hash baked in`,
|
|
481
486
|
``,
|
|
482
487
|
].join("\n");
|
|
483
488
|
writeFileSync(path.join(outDir, `${cfg.name}.summary.txt`), summaryTxt, "utf8");
|
|
@@ -519,9 +524,10 @@ async function main(): Promise<void> {
|
|
|
519
524
|
console.log(` deploy ${rel(deployPath)}`);
|
|
520
525
|
console.log(``);
|
|
521
526
|
console.log(`deploy (Nomos Cloud):`);
|
|
522
|
-
console.log(`
|
|
523
|
-
console.log(`
|
|
524
|
-
console.log(`
|
|
527
|
+
console.log(` githolon ws create <ws> && githolon deploy <ws> # the secret is saved + sent for you`);
|
|
528
|
+
console.log(` # raw lane: curl -X POST -H 'content-type: application/json' -H 'Authorization: Bearer <workspaceSecret>' \\`);
|
|
529
|
+
console.log(` # --data-binary @${rel(deployPath)} https://nomos.captainapp.co.uk/v1/workspaces/<ws>/domains`);
|
|
530
|
+
console.log(`prove it: npm run e2e # offline write -> sync -> admission -> cloud query, live`);
|
|
525
531
|
}
|
|
526
532
|
|
|
527
533
|
await main();
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
7
7
|
|
|
8
8
|
/** `@githolon/dsl` — the Nomos 2 domain-authoring DSL. */
|
|
9
|
+
// ONE import for a domain file: `z` (payload schemas) rides along with the DSL, so
|
|
10
|
+
// authors never juggle a separate zod dependency (or risk a second zod instance).
|
|
11
|
+
export { z } from "zod";
|
|
9
12
|
export {
|
|
10
13
|
aggregate,
|
|
11
14
|
instance,
|