@githolon/dsl 0.48.1 → 0.50.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.
@@ -52,6 +52,7 @@
52
52
  */
53
53
  import { z } from "zod";
54
54
 
55
+ import type { Driver } from "./drivers.js";
55
56
  import {
56
57
  valueObjectField,
57
58
  type Field,
@@ -129,6 +130,17 @@ export interface ValueObject<T = unknown> {
129
130
  * whole), `.optional()`, `.encrypted()` (under the existing `requires(cap)` rules).
130
131
  */
131
132
  field(): Field<T, "json">;
133
+ /**
134
+ * SUGAR for `.field().merge(driver)` — lower this VO DIRECTLY to an aggregate field with
135
+ * the chosen merge driver: `location: SiteLocation.merge(Lww).optional()`. A VO field is
136
+ * ONE Lww-class json leaf replaced WHOLESALE, so only the Lww-class family
137
+ * (`Lww` / `LwwCausal` / `LwwBy`) is accepted — any other driver throws (field-level merge
138
+ * composition of a VO's subfields is a later slice; model those as aggregate fields / a
139
+ * `t.map(...)`). The returned `Field` chains the usual modifiers (`.optional()`,
140
+ * `.encrypted()`, …), so `SiteLocation.merge(Lww).optional()` == `SiteLocation.field()
141
+ * .merge(Lww).optional()` byte-for-byte.
142
+ */
143
+ merge(driver: Driver): Field<T, "json">;
132
144
  /** Phantom to keep T load-bearing for inference. */
133
145
  readonly _t?: T;
134
146
  }
@@ -255,12 +267,48 @@ export function idOfMetaOf(zt: z.ZodTypeAny): string | undefined {
255
267
  return undefined;
256
268
  }
257
269
 
270
+ /** Thread a nested VO's own descriptor + transitive dependencies into `deps` (divergent
271
+ * same-name shapes throw — one name, one shape), then return its ref field descriptor. This
272
+ * is what makes `z.array(SomeVo.zod)` / a raw `SomeVo.zod` entry plug-and-play: the VO brand
273
+ * rides the `.zod`, so the descriptor TREE is threaded rather than lost (vo_first_class.md
274
+ * slice 0). */
275
+ function takeVoField(
276
+ meta: ValueObjectFieldMeta,
277
+ deps: Map<string, ValueObjectDescriptor>,
278
+ ): ValueObjectFieldDescriptor {
279
+ for (const d of [meta.descriptor, ...meta.dependencies]) {
280
+ const existing = deps.get(d.name);
281
+ if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(d)) {
282
+ throw new Error(
283
+ `valueObject: two DIVERGENT value objects share the name '${d.name}' — one name, one shape. ` +
284
+ `Rename one of them (the descriptor is identity-bearing).`,
285
+ );
286
+ }
287
+ deps.set(d.name, d);
288
+ }
289
+ return meta.geometry !== undefined
290
+ ? { type: "geometry", ref: meta.name, geometry: meta.geometry }
291
+ : { type: "object", ref: meta.name };
292
+ }
293
+
258
294
  /**
259
295
  * Describe one CONSTRAINED zod schema as a descriptor field (NO FALLBACK — an
260
296
  * out-of-set shape throws with the field's path so the author fixes the declaration,
261
- * never ships an undescribed structure).
297
+ * never ships an undescribed structure). `deps` collects every nested VO the schema
298
+ * references BY BRAND (`z.array(SomeVo.zod)`, a raw `SomeVo.zod`) so its descriptor rides
299
+ * alongside — the caller unions it into the VO's `dependencies`.
262
300
  */
263
- function describeZod(voName: string, path: string, schema: z.ZodTypeAny): ValueObjectFieldDescriptor {
301
+ function describeZod(
302
+ voName: string,
303
+ path: string,
304
+ schema: z.ZodTypeAny,
305
+ deps: Map<string, ValueObjectDescriptor>,
306
+ ): ValueObjectFieldDescriptor {
307
+ // A VO referenced through its RAW zod (its own brand, no optional wrapper) — emit the
308
+ // ref + thread its descriptor tree. (Optional wrappers are handled by the switch below,
309
+ // which recurses to the branded inner schema.)
310
+ const ownBrand = (schema as unknown as Record<symbol, unknown>)[NOMOS_VO_BRAND];
311
+ if (ownBrand !== undefined) return takeVoField(ownBrand as ValueObjectFieldMeta, deps);
264
312
  switch (zodKind(schema)) {
265
313
  case "string":
266
314
  return { type: "string" };
@@ -285,7 +333,7 @@ function describeZod(voName: string, path: string, schema: z.ZodTypeAny): ValueO
285
333
  return { type: "literal", value: v };
286
334
  }
287
335
  case "array": {
288
- const items = describeZod(voName, `${path}[]`, zodArrayElement(schema));
336
+ const items = describeZod(voName, `${path}[]`, zodArrayElement(schema), deps);
289
337
  if (items.optional === true) {
290
338
  throw new Error(
291
339
  `valueObject '${voName}': field '${path}' is an array of optionals — not expressible in the v1 set.`,
@@ -295,7 +343,7 @@ function describeZod(voName: string, path: string, schema: z.ZodTypeAny): ValueO
295
343
  }
296
344
  case "optional":
297
345
  case "default": {
298
- const inner = describeZod(voName, path, zodDef(schema).innerType!);
346
+ const inner = describeZod(voName, path, zodDef(schema).innerType!, deps);
299
347
  return { ...inner, optional: true };
300
348
  }
301
349
  default:
@@ -452,7 +500,29 @@ function makeValueObject<T>(
452
500
  field(): Field<T, "json"> {
453
501
  return valueObjectField(zodSchema, meta);
454
502
  },
503
+ merge(driver: Driver): Field<T, "json"> {
504
+ const k = driver.kind;
505
+ if (k !== "Lww" && k !== "LwwCausal" && k !== "LwwBy") {
506
+ throw new Error(
507
+ `valueObject '${name}': .merge(${k}) is not a valid value-object field merge — a VO is ONE ` +
508
+ `Lww-class json leaf replaced WHOLESALE, so only Lww / LwwCausal / LwwBy apply. Field-level ` +
509
+ `merge composition of a VO's subfields is a later slice; model independently-merging subfields ` +
510
+ `as aggregate fields or a t.map(...).`,
511
+ );
512
+ }
513
+ return valueObjectField(zodSchema, meta).merge(driver);
514
+ },
455
515
  };
516
+ // THE HANDLE IS A FIRST-CLASS ZOD SCHEMA (vo_first_class.md slice 1): delegate zod's
517
+ // internals so the raw HANDLE composes DIRECTLY where a zod is expected — `z.array(Vo)`,
518
+ // `z.object({ f: Vo })`, a directive payload — with NO `.zod` peeling. Non-enumerable (never
519
+ // serialized, domainHash-inert) and stamped with the VO brand so describeZod / voMetaOf resolve
520
+ // the handle exactly like its `.zod`.
521
+ const inner = zodSchema as unknown as { _zod?: unknown; _def?: unknown; def?: unknown };
522
+ Object.defineProperty(vo, "_zod", { get: () => inner._zod, enumerable: false });
523
+ Object.defineProperty(vo, "_def", { get: () => inner._def, enumerable: false });
524
+ Object.defineProperty(vo, "def", { get: () => inner.def, enumerable: false });
525
+ Object.defineProperty(vo, NOMOS_VO_BRAND, { value: meta, enumerable: false });
456
526
  return vo;
457
527
  }
458
528
 
@@ -495,7 +565,7 @@ export function valueObject<const Name extends string, S extends Record<string,
495
565
  : { type: "object", ref: entry.name };
496
566
  collectDescriptors(entry, deps);
497
567
  } else {
498
- fields[key] = describeZod(name, key, entry);
568
+ fields[key] = describeZod(name, key, entry, deps);
499
569
  zodShape[key] = entry;
500
570
  }
501
571
  }
@@ -584,7 +654,7 @@ export function variant<const Name extends string, Arms extends Record<string, U
584
654
  : { type: "object", ref: entry.name };
585
655
  collectDescriptors(entry, deps);
586
656
  } else {
587
- fields[fieldKey] = describeZod(name, `${key}.${fieldKey}`, entry);
657
+ fields[fieldKey] = describeZod(name, `${key}.${fieldKey}`, entry, deps);
588
658
  zodShape[fieldKey] = entry;
589
659
  }
590
660
  }