@githolon/dsl 0.31.1 → 0.33.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.
@@ -0,0 +1,413 @@
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
+ * FIRST-CLASS COMPOSABLE VALUE OBJECTS — `valueObject(name, shape)` + the `geo.*`
10
+ * geometry VOs.
11
+ *
12
+ * THE TENANT GAP THIS CLOSES: a structured value on an aggregate was a hand-rolled
13
+ * `t.jsonObject()` plus a privately-kept zod schema per site — untyped in the generated
14
+ * clients, invisible in the package IR. A value object is the NAMED, REUSABLE,
15
+ * COMPOSABLE version of that same leaf:
16
+ *
17
+ * const PostalAddress = valueObject("PostalAddress", {
18
+ * streetAddress: z.string(), city: z.string(), countryCode: z.string(),
19
+ * });
20
+ * const GeoPoint = geo.point("GeoPoint"); // RFC 7946 Point
21
+ * const SiteLocation = valueObject("SiteLocation", {
22
+ * address: PostalAddress.optional(),
23
+ * point: GeoPoint.optional(),
24
+ * });
25
+ * // on an aggregate:
26
+ * location: SiteLocation.field().optional(),
27
+ *
28
+ * ONE definition produces: the zod schema (the SAME zod the engine validates — embed
29
+ * `SiteLocation.zod` in a directive payload), the inferred TS type (`SiteLocation.field()`
30
+ * types `set()` and the generated `.client.ts` names the interface), the generated Dart
31
+ * class (typed decode from the projection's json-object value), and a canonical
32
+ * descriptor that rides the package IR (the additive `nomosValueObjects` USD key) so
33
+ * tooling/visualizers can SEE the structure.
34
+ *
35
+ * STORAGE / MERGE SEMANTICS (the v1 boundary — state it, don't blur it): a VO field IS
36
+ * the existing `t.jsonObject()` value-leaf lane — ONE Lww-class leaf, replaced WHOLESALE
37
+ * on write (exactly the encrypted-envelope rule). There is NO per-subfield merge: two
38
+ * concurrent writes to different subfields of one VO converge to ONE writer's whole
39
+ * value. When subfields must merge independently, model them as aggregate fields or a
40
+ * `t.map(...)` — that is what aggregates/maps are for.
41
+ *
42
+ * ERA SAFETY: the VO metadata never enters the kernel wire schema or the canonical
43
+ * manifest — a domain's domainHash with a VO field equals the same domain with a plain
44
+ * `t.jsonObject()` field. The `nomosValueObjects` USD key is emitted IFF a domain's
45
+ * aggregates carry ≥1 VO field; a VO-free domain's package bytes are untouched.
46
+ *
47
+ * THE v1 CONSTRAINED FIELD SET (no fallback — anything else throws at compile):
48
+ * `z.string()` · `z.number()` (int or float) · `z.boolean()` · `z.enum([...])` ·
49
+ * `z.array(<any of these>)` · a nested `ValueObject` · and `.optional()` on any of
50
+ * them (arrays of optionals excluded). Unions, records, maps, tuples, dates etc. are
51
+ * deliberately OUT — this surface is forever; grow it deliberately.
52
+ */
53
+ import { z } from "zod";
54
+
55
+ import {
56
+ valueObjectField,
57
+ type Field,
58
+ type GeometryKind,
59
+ type ValueObjectDescriptor,
60
+ type ValueObjectFieldDescriptor,
61
+ type ValueObjectFieldMeta,
62
+ } from "./fields.js";
63
+ // LOCAL zod introspection (the same `_def ?? def` convention as codegen_ts.ts) —
64
+ // deliberately NOT imported from codegen_ts.js: this module rides the RUNTIME barrel
65
+ // (the engine lump bundles it, `--platform=neutral`), and codegen_ts transitively
66
+ // imports `node:crypto` via workspace_routing. Keep this file node-builtin-free.
67
+ type ZodInternalDef = {
68
+ type?: string | z.ZodTypeAny;
69
+ innerType?: z.ZodTypeAny;
70
+ element?: z.ZodTypeAny;
71
+ entries?: Record<string, string | number>;
72
+ };
73
+ function zodDef(zt: z.ZodTypeAny): ZodInternalDef {
74
+ const raw = zt as unknown as { _def?: ZodInternalDef; def?: ZodInternalDef };
75
+ return raw._def ?? raw.def ?? {};
76
+ }
77
+ function zodKind(zt: z.ZodTypeAny): string {
78
+ const t = zodDef(zt).type;
79
+ return typeof t === "string" ? t : "unknown";
80
+ }
81
+ function zodEnumValues(zt: z.ZodTypeAny): string[] {
82
+ const raw = zt as unknown as { options?: readonly unknown[] };
83
+ if (raw.options !== undefined) return raw.options.map(String);
84
+ const entries = zodDef(zt).entries;
85
+ if (entries !== undefined) return Object.values(entries).map(String);
86
+ throw new Error("valueObject: ZodEnum has no values");
87
+ }
88
+ function zodArrayElement(zt: z.ZodTypeAny): z.ZodTypeAny {
89
+ const def = zodDef(zt);
90
+ if (def.element !== undefined) return def.element;
91
+ if (typeof def.type !== "string" && def.type !== undefined) return def.type;
92
+ throw new Error("valueObject: ZodArray has no element type");
93
+ }
94
+
95
+ export type { GeometryKind, ValueObjectDescriptor, ValueObjectFieldDescriptor } from "./fields.js";
96
+
97
+ /** An `.optional()`-wrapped value object — valid ONLY inside a `valueObject(...)` shape. */
98
+ export interface OptionalValueObject<T = unknown> {
99
+ readonly __isOptionalValueObject: true;
100
+ readonly valueObject: ValueObject<T>;
101
+ }
102
+
103
+ /**
104
+ * A NAMED, COMPOSABLE value object — produced by {@link valueObject} / {@link geo}.
105
+ * Carries the zod (validation + TS inference), the canonical descriptor (the IR/codegen
106
+ * shape), and lowers onto an aggregate via {@link ValueObject.field}.
107
+ */
108
+ export interface ValueObject<T = unknown> {
109
+ readonly __isValueObject: true;
110
+ /** The declared name — the generated TS interface / Dart class name. */
111
+ readonly name: string;
112
+ /** The zod schema — the single source of truth (embed it in directive payloads). */
113
+ readonly zod: z.ZodType<T>;
114
+ /** This VO's own canonical descriptor (field names sorted; byte-deterministic). */
115
+ readonly descriptor: ValueObjectDescriptor;
116
+ /** Every transitively nested VO's descriptor (self excluded), sorted by name. */
117
+ readonly dependencies: readonly ValueObjectDescriptor[];
118
+ /** Present iff this is a `geo.*` geometry VO (the spatial-index pairing). */
119
+ readonly geometry?: GeometryKind;
120
+ /** Mark this VO optional INSIDE another `valueObject(...)` shape (nesting only —
121
+ * for an optional aggregate FIELD use `.field().optional()`). */
122
+ optional(): OptionalValueObject<T>;
123
+ /**
124
+ * Lower this VO to an aggregate FIELD: one `t.jsonObject()`-class value leaf
125
+ * (kind `json`, `jsonShape:"object"`, Lww) whose whole value is this VO —
126
+ * WHOLESALE-REPLACE merge, no per-subfield merge (v1). Chain the usual field
127
+ * modifiers: `.merge(Lww/LwwCausal/LwwBy)` (Lww-class only — the value is replaced
128
+ * whole), `.optional()`, `.encrypted()` (under the existing `requires(cap)` rules).
129
+ */
130
+ field(): Field<T, "json">;
131
+ /** Phantom to keep T load-bearing for inference. */
132
+ readonly _t?: T;
133
+ }
134
+
135
+ /** Infer the authored TS type of a ValueObject. */
136
+ export type ValueObjectValue<V> = V extends ValueObject<infer T> ? T : never;
137
+
138
+ /** A `valueObject(...)` shape entry: a constrained zod schema, a nested VO, or an optional VO. */
139
+ export type ValueObjectShapeEntry = z.ZodTypeAny | ValueObject<unknown> | OptionalValueObject<unknown>;
140
+
141
+ type EntryValue<E> =
142
+ E extends OptionalValueObject<infer T> ? T | undefined
143
+ : E extends ValueObject<infer T> ? T
144
+ : E extends z.ZodType<infer T> ? T
145
+ : never;
146
+
147
+ type OptionalKeys<S> = {
148
+ [K in keyof S]: undefined extends EntryValue<S[K]> ? K : never;
149
+ }[keyof S];
150
+
151
+ /** The inferred value type of a shape: optional entries become optional properties. */
152
+ export type ShapeValue<S> = { [K in Exclude<keyof S, OptionalKeys<S>>]: EntryValue<S[K]> } & {
153
+ [K in OptionalKeys<S>]?: EntryValue<S[K]>;
154
+ };
155
+
156
+ const isValueObject = (v: unknown): v is ValueObject<unknown> =>
157
+ typeof v === "object" && v !== null && (v as { __isValueObject?: boolean }).__isValueObject === true;
158
+
159
+ const isOptionalValueObject = (v: unknown): v is OptionalValueObject<unknown> =>
160
+ typeof v === "object" &&
161
+ v !== null &&
162
+ (v as { __isOptionalValueObject?: boolean }).__isOptionalValueObject === true;
163
+
164
+ /** Collect a VO's descriptor + its transitive dependencies into `into` (name-keyed). */
165
+ function collectDescriptors(vo: ValueObject<unknown>, into: Map<string, ValueObjectDescriptor>): void {
166
+ const existing = into.get(vo.name);
167
+ if (existing !== undefined) {
168
+ if (JSON.stringify(existing) !== JSON.stringify(vo.descriptor)) {
169
+ throw new Error(
170
+ `valueObject: two DIVERGENT value objects share the name '${vo.name}' — one name, one shape. ` +
171
+ `Rename one of them (the descriptor is identity-bearing).`,
172
+ );
173
+ }
174
+ return;
175
+ }
176
+ into.set(vo.name, vo.descriptor);
177
+ for (const dep of vo.dependencies) {
178
+ const prior = into.get(dep.name);
179
+ if (prior !== undefined && JSON.stringify(prior) !== JSON.stringify(dep)) {
180
+ throw new Error(
181
+ `valueObject: two DIVERGENT value objects share the name '${dep.name}' — one name, one shape. ` +
182
+ `Rename one of them (the descriptor is identity-bearing).`,
183
+ );
184
+ }
185
+ into.set(dep.name, dep);
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Describe one CONSTRAINED zod schema as a descriptor field (NO FALLBACK — an
191
+ * out-of-set shape throws with the field's path so the author fixes the declaration,
192
+ * never ships an undescribed structure).
193
+ */
194
+ function describeZod(voName: string, path: string, schema: z.ZodTypeAny): ValueObjectFieldDescriptor {
195
+ switch (zodKind(schema)) {
196
+ case "string":
197
+ return { type: "string" };
198
+ case "number":
199
+ return { type: "number" };
200
+ case "boolean":
201
+ return { type: "boolean" };
202
+ case "enum":
203
+ return { type: "enum", values: zodEnumValues(schema) };
204
+ case "array": {
205
+ const items = describeZod(voName, `${path}[]`, zodArrayElement(schema));
206
+ if (items.optional === true) {
207
+ throw new Error(
208
+ `valueObject '${voName}': field '${path}' is an array of optionals — not expressible in the v1 set.`,
209
+ );
210
+ }
211
+ return { type: "array", items };
212
+ }
213
+ case "optional":
214
+ case "default": {
215
+ const inner = describeZod(voName, path, zodDef(schema).innerType!);
216
+ return { ...inner, optional: true };
217
+ }
218
+ default:
219
+ throw new Error(
220
+ `valueObject '${voName}': field '${path}' uses zod shape '${zodKind(schema)}', which is outside ` +
221
+ `the v1 value-object field set (string | number | boolean | enum | array | nested valueObject, ` +
222
+ `each optionally .optional()). Model richer shapes as aggregates/maps, or nest a valueObject.`,
223
+ );
224
+ }
225
+ }
226
+
227
+ const VO_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
228
+
229
+ function makeValueObject<T>(
230
+ name: string,
231
+ zodSchema: z.ZodType<T>,
232
+ descriptor: ValueObjectDescriptor,
233
+ dependencies: readonly ValueObjectDescriptor[],
234
+ geometry?: GeometryKind,
235
+ ): ValueObject<T> {
236
+ if (!VO_NAME_RE.test(name)) {
237
+ throw new Error(
238
+ `valueObject: name '${name}' is not a valid identifier ([A-Za-z][A-Za-z0-9_]*) — it becomes the ` +
239
+ `generated TS interface / Dart class name.`,
240
+ );
241
+ }
242
+ const meta: ValueObjectFieldMeta = {
243
+ name,
244
+ descriptor,
245
+ dependencies,
246
+ ...(geometry !== undefined ? { geometry } : {}),
247
+ };
248
+ const vo: ValueObject<T> = {
249
+ __isValueObject: true,
250
+ name,
251
+ zod: zodSchema,
252
+ descriptor,
253
+ dependencies,
254
+ ...(geometry !== undefined ? { geometry } : {}),
255
+ optional(): OptionalValueObject<T> {
256
+ return { __isOptionalValueObject: true, valueObject: vo };
257
+ },
258
+ field(): Field<T, "json"> {
259
+ return valueObjectField(zodSchema, meta);
260
+ },
261
+ };
262
+ return vo;
263
+ }
264
+
265
+ /**
266
+ * Declare a NAMED, COMPOSABLE value object. `shape` entries come from the v1
267
+ * constrained set (see the module banner): plain zod scalars/enums/arrays, nested
268
+ * `ValueObject`s, or `SomeVo.optional()`. Returns a {@link ValueObject} carrying the
269
+ * zod schema, the inferred TS type, the canonical descriptor (+ transitive nested-VO
270
+ * descriptors), and `.field()` to place it on an aggregate.
271
+ */
272
+ export function valueObject<const Name extends string, S extends Record<string, ValueObjectShapeEntry>>(
273
+ name: Name,
274
+ shape: S,
275
+ ): ValueObject<ShapeValue<S>> {
276
+ const zodShape: Record<string, z.ZodTypeAny> = {};
277
+ const fields: Record<string, ValueObjectFieldDescriptor> = {};
278
+ const deps = new Map<string, ValueObjectDescriptor>();
279
+ for (const key of Object.keys(shape).sort()) {
280
+ const entry = shape[key]!;
281
+ if (isOptionalValueObject(entry)) {
282
+ const inner = entry.valueObject;
283
+ zodShape[key] = (inner.zod as z.ZodTypeAny).optional();
284
+ fields[key] =
285
+ inner.geometry !== undefined
286
+ ? { type: "geometry", ref: inner.name, geometry: inner.geometry, optional: true }
287
+ : { type: "object", ref: inner.name, optional: true };
288
+ collectDescriptors(inner, deps);
289
+ } else if (isValueObject(entry)) {
290
+ zodShape[key] = entry.zod as z.ZodTypeAny;
291
+ fields[key] =
292
+ entry.geometry !== undefined
293
+ ? { type: "geometry", ref: entry.name, geometry: entry.geometry }
294
+ : { type: "object", ref: entry.name };
295
+ collectDescriptors(entry, deps);
296
+ } else {
297
+ fields[key] = describeZod(name, key, entry);
298
+ zodShape[key] = entry;
299
+ }
300
+ }
301
+ if (deps.has(name)) {
302
+ throw new Error(`valueObject '${name}': a value object cannot nest itself (directly or via a dependency).`);
303
+ }
304
+ const descriptor: ValueObjectDescriptor = { name, fields };
305
+ const dependencies = [...deps.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
306
+ return makeValueObject(
307
+ name,
308
+ z.object(zodShape) as unknown as z.ZodType<ShapeValue<S>>,
309
+ descriptor,
310
+ dependencies,
311
+ );
312
+ }
313
+
314
+ // ── geo.* — the RFC 7946 geometry value objects ──────────────────────────────────────
315
+
316
+ /** An RFC 7946 position: `[lng, lat]` or `[lng, lat, altitude]`. */
317
+ const positionZod = z.array(z.number()).min(2).max(3);
318
+
319
+ /** The authored TS shape of a `geo.point(...)` value (RFC 7946 Point). */
320
+ export interface GeoPointValue {
321
+ type: "Point";
322
+ /** `[lng, lat]` or `[lng, lat, altitude]` (RFC 7946 position order). */
323
+ coordinates: number[];
324
+ }
325
+ /** The authored TS shape of a `geo.lineString(...)` value (RFC 7946 LineString). */
326
+ export interface GeoLineStringValue {
327
+ type: "LineString";
328
+ coordinates: number[][];
329
+ }
330
+ /** The authored TS shape of a `geo.polygon(...)` value (RFC 7946 Polygon). */
331
+ export interface GeoPolygonValue {
332
+ type: "Polygon";
333
+ coordinates: number[][][];
334
+ }
335
+
336
+ function geoZod(kind: GeometryKind, coordinates: z.ZodTypeAny): z.ZodTypeAny {
337
+ return z.object({ type: z.literal(kind), coordinates });
338
+ }
339
+
340
+ /**
341
+ * The GEO value objects — RFC 7946 geometries as named VOs, and THE BLESSED WAY to
342
+ * declare a spatial-indexable field: give the geometry its OWN aggregate field
343
+ * (`point: GeoPoint.field()`) and pair it with `spatial(id).of(Agg).on("point")` —
344
+ * the R*Tree indexes the field's GeoJSON geometry directly. (v1 boundary: the spatial
345
+ * lane indexes a TOP-LEVEL field; a geometry nested inside another VO — e.g.
346
+ * `location.point` — is data, not an indexable path.)
347
+ */
348
+ export const geo = {
349
+ /** An RFC 7946 **Point** (`{type:"Point", coordinates:[lng,lat(,alt)?]}`) — the v1
350
+ * spatial-index geometry. */
351
+ point(name = "GeoPoint"): ValueObject<GeoPointValue> {
352
+ return makeValueObject<GeoPointValue>(
353
+ name,
354
+ geoZod("Point", positionZod) as z.ZodType<GeoPointValue>,
355
+ { name, geometry: "Point" },
356
+ [],
357
+ "Point",
358
+ );
359
+ },
360
+ /** An RFC 7946 **LineString** (≥2 positions). */
361
+ lineString(name = "GeoLineString"): ValueObject<GeoLineStringValue> {
362
+ return makeValueObject<GeoLineStringValue>(
363
+ name,
364
+ geoZod("LineString", z.array(positionZod).min(2)) as z.ZodType<GeoLineStringValue>,
365
+ { name, geometry: "LineString" },
366
+ [],
367
+ "LineString",
368
+ );
369
+ },
370
+ /** An RFC 7946 **Polygon** (linear rings — ≥4 positions each, first == last by RFC;
371
+ * ring closure is the author's contract, not re-validated here). */
372
+ polygon(name = "GeoPolygon"): ValueObject<GeoPolygonValue> {
373
+ return makeValueObject<GeoPolygonValue>(
374
+ name,
375
+ geoZod("Polygon", z.array(z.array(positionZod).min(4))) as z.ZodType<GeoPolygonValue>,
376
+ { name, geometry: "Polygon" },
377
+ [],
378
+ "Polygon",
379
+ );
380
+ },
381
+ };
382
+
383
+ // ── module-plane collection (composeDomainModule / codegen) ──────────────────────────
384
+
385
+ /**
386
+ * Collect the DEDUPED, SORTED value-object descriptors used by a set of aggregates
387
+ * (each VO field's own descriptor + its transitive dependencies). Divergent same-name
388
+ * descriptors throw (one name, one shape). Returns `[]` when no field carries a VO —
389
+ * the omit-when-empty input to the `nomosValueObjects` era key.
390
+ */
391
+ export function collectValueObjectDescriptors(
392
+ aggregates: readonly { id: string; fields: Record<string, Field> }[],
393
+ ): ValueObjectDescriptor[] {
394
+ const byName = new Map<string, ValueObjectDescriptor>();
395
+ for (const agg of aggregates) {
396
+ for (const field of Object.values(agg.fields)) {
397
+ const meta = field.valueObject;
398
+ if (meta === undefined) continue;
399
+ const all = [meta.descriptor, ...meta.dependencies];
400
+ for (const d of all) {
401
+ const existing = byName.get(d.name);
402
+ if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(d)) {
403
+ throw new Error(
404
+ `valueObject: aggregate '${agg.id}' uses a value object named '${d.name}' whose shape DIVERGES ` +
405
+ `from another use of the same name — one name, one shape. Rename one of them.`,
406
+ );
407
+ }
408
+ byName.set(d.name, d);
409
+ }
410
+ }
411
+ }
412
+ return [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
413
+ }