@githolon/dsl 0.53.0 → 0.54.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.
@@ -44,11 +44,12 @@
44
44
  * `t.jsonObject()` field. The `nomosValueObjects` USD key is emitted IFF a domain's
45
45
  * aggregates carry ≥1 VO field; a VO-free domain's package bytes are untouched.
46
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.
47
+ * THE CONSTRAINED FIELD SET (no fallback — anything else throws at compile):
48
+ * `z.string()` (plain, `brandedId(...)`, or `idOf(...)`) · `z.number()` (int/float/`unit(...)`) ·
49
+ * `z.boolean()` · `z.enum([...])` · `z.literal("x")` · `z.array(<any of these>)` ·
50
+ * `z.record(z.string(), <any of these>)` · a nested `ValueObject` · a `variant(...)` sum ·
51
+ * and `.optional()` on any of them (arrays/records of optionals excluded). Tuples, dates,
52
+ * non-string-keyed records etc. are deliberately OUT — grow this surface deliberately.
52
53
  */
53
54
  import { z } from "zod";
54
55
 
@@ -71,6 +72,8 @@ type ZodInternalDef = {
71
72
  innerType?: z.ZodTypeAny;
72
73
  element?: z.ZodTypeAny;
73
74
  entries?: Record<string, string | number>;
75
+ valueType?: z.ZodTypeAny;
76
+ keyType?: z.ZodTypeAny;
74
77
  };
75
78
  function zodDef(zt: z.ZodTypeAny): ZodInternalDef {
76
79
  const raw = zt as unknown as { _def?: ZodInternalDef; def?: ZodInternalDef };
@@ -93,6 +96,17 @@ function zodArrayElement(zt: z.ZodTypeAny): z.ZodTypeAny {
93
96
  if (typeof def.type !== "string" && def.type !== undefined) return def.type;
94
97
  throw new Error("valueObject: ZodArray has no element type");
95
98
  }
99
+ /** The VALUE schema of a `z.record(z.string(), V)` (across zod v4's def encodings). */
100
+ function zodRecordValue(zt: z.ZodTypeAny): z.ZodTypeAny {
101
+ const def = zodDef(zt);
102
+ if (def.valueType !== undefined) return def.valueType;
103
+ throw new Error("valueObject: ZodRecord has no value type");
104
+ }
105
+ /** The KEY schema of a `z.record(K, V)` — v1 requires a plain `z.string()` key. */
106
+ function zodRecordKeyKind(zt: z.ZodTypeAny): string {
107
+ const def = zodDef(zt);
108
+ return def.keyType !== undefined ? zodKind(def.keyType) : "string";
109
+ }
96
110
 
97
111
  export type { GeometryKind, ValueObjectDescriptor, ValueObjectFieldDescriptor } from "./fields.js";
98
112
 
@@ -254,6 +268,43 @@ export function idOf(aggregate: { id: string } | string): z.ZodType<string> {
254
268
  return schema;
255
269
  }
256
270
 
271
+ /**
272
+ * THE BRANDED-ID BRAND (dart_codegen_parity.md P2 — the standalone twin of {@link idOf}) —
273
+ * `brandedId("SiteId")` is a NOMINAL string: wire-identical to `z.string()` (validation +
274
+ * domainHash untouched) but the generated TS is a branded alias
275
+ * (`type SiteId = string & { readonly __brand }`) and the generated Dart a zero-cost
276
+ * extension type (`extension type const SiteId(String value) {}`), so passing a bare string
277
+ * — or another brand — where a `SiteId` belongs is a COMPILE error. Unlike `idOf`, it is NOT
278
+ * bound to an aggregate handle: use it for any nominal string id (external refs, opaque
279
+ * tokens) inside VO shapes / payloads. Identity + codec only — no generated behaviour.
280
+ */
281
+ export const NOMOS_BRANDED_ID_BRAND: unique symbol = Symbol.for("nomos.brandedId");
282
+
283
+ const BRAND_NAME_RE = /^[A-Z][A-Za-z0-9_]*$/;
284
+
285
+ /** Declare a NOMINAL string id: `siteId: brandedId("SiteId")`. */
286
+ export function brandedId(name: string): z.ZodType<string> {
287
+ if (!BRAND_NAME_RE.test(name)) {
288
+ throw new Error(`brandedId: '${name}' must be a capitalized identifier — it becomes the generated branded type's name.`);
289
+ }
290
+ const schema = z.string();
291
+ Object.defineProperty(schema, NOMOS_BRANDED_ID_BRAND, { value: name, enumerable: false });
292
+ return schema;
293
+ }
294
+
295
+ /** Resolve the `brandedId(...)` brand name of a schema (through optional/default/nullable). */
296
+ export function brandedIdMetaOf(zt: z.ZodTypeAny): string | undefined {
297
+ let cur: z.ZodTypeAny | undefined = zt;
298
+ for (let hops = 0; cur !== undefined && hops < 16; hops++) {
299
+ const branded = (cur as unknown as Record<symbol, unknown>)[NOMOS_BRANDED_ID_BRAND];
300
+ if (typeof branded === "string") return branded;
301
+ const kind = zodKind(cur);
302
+ if (kind !== "optional" && kind !== "default" && kind !== "nullable") return undefined;
303
+ cur = zodDef(cur).innerType;
304
+ }
305
+ return undefined;
306
+ }
307
+
257
308
  /** Resolve the `idOf(...)` aggregate type of a payload schema (through optional/default). */
258
309
  export function idOfMetaOf(zt: z.ZodTypeAny): string | undefined {
259
310
  let cur: z.ZodTypeAny | undefined = zt;
@@ -310,8 +361,28 @@ function describeZod(
310
361
  const ownBrand = (schema as unknown as Record<symbol, unknown>)[NOMOS_VO_BRAND];
311
362
  if (ownBrand !== undefined) return takeVoField(ownBrand as ValueObjectFieldMeta, deps);
312
363
  switch (zodKind(schema)) {
313
- case "string":
314
- return { type: "string" };
364
+ case "string": {
365
+ const brand = (schema as unknown as Record<symbol, unknown>)[NOMOS_BRANDED_ID_BRAND]
366
+ ?? (schema as unknown as Record<symbol, unknown>)[NOMOS_IDOF_BRAND];
367
+ // idOf's brand is the aggregate type name; a payload idOf surfaces as `<Agg>Id`.
368
+ const brandName =
369
+ typeof (schema as unknown as Record<symbol, unknown>)[NOMOS_BRANDED_ID_BRAND] === "string"
370
+ ? (brand as string)
371
+ : typeof brand === "string"
372
+ ? `${brand}Id`
373
+ : undefined;
374
+ return brandName !== undefined ? { type: "string", brand: brandName } : { type: "string" };
375
+ }
376
+ case "record": {
377
+ if (zodRecordKeyKind(schema) !== "string") {
378
+ throw new Error(`valueObject '${voName}': field '${path}' — only string-keyed records (z.record(z.string(), …)) are in the v1 set.`);
379
+ }
380
+ const values = describeZod(voName, `${path}{}`, zodRecordValue(schema), deps);
381
+ if (values.optional === true) {
382
+ throw new Error(`valueObject '${voName}': field '${path}' is a record of optionals — not expressible in the v1 set.`);
383
+ }
384
+ return { type: "map", values };
385
+ }
315
386
  case "number": {
316
387
  const unitName = (schema as unknown as Record<symbol, unknown>)[NOMOS_UNIT_BRAND];
317
388
  return {
@@ -391,6 +462,7 @@ const GEOMETRY_SAMPLES: Record<GeometryKind, unknown> = {
391
462
  Point: { type: "Point", coordinates: [0, 0] },
392
463
  LineString: { type: "LineString", coordinates: [[0, 0], [1, 1]] },
393
464
  Polygon: { type: "Polygon", coordinates: [[[0, 0], [0, 1], [1, 1], [0, 0]]] },
465
+ MultiPoint: { type: "MultiPoint", coordinates: [[0, 0], [1, 1]] },
394
466
  };
395
467
 
396
468
  function sampleOfDescriptorField(
@@ -398,6 +470,7 @@ function sampleOfDescriptorField(
398
470
  fieldName: string,
399
471
  byName: Map<string, ValueObjectDescriptor>,
400
472
  includeOptional = false,
473
+ path = fieldName,
401
474
  ): unknown {
402
475
  switch (f.type) {
403
476
  case "string": return `sample-${fieldName}`;
@@ -405,37 +478,43 @@ function sampleOfDescriptorField(
405
478
  case "literal": return f.value;
406
479
  case "boolean": return true;
407
480
  case "enum": return f.values[0];
408
- case "array": return [sampleOfDescriptorField(f.items, fieldName, byName, includeOptional)];
481
+ case "array": return [sampleOfDescriptorField(f.items, fieldName, byName, includeOptional, `${path}[0]`)];
482
+ case "map": return { key: sampleOfDescriptorField(f.values, fieldName, byName, includeOptional, `${path}.key`) };
409
483
  case "geometry": return GEOMETRY_SAMPLES[f.geometry];
410
484
  case "object": {
411
485
  const dep = byName.get(f.ref);
412
- if (dep === undefined) throw new Error(`voSample: nested value object '${f.ref}' has no descriptor`);
413
- return sampleOfDescriptor(dep, byName, includeOptional);
486
+ if (dep === undefined) throw new Error(`voSample: field '${path}' — nested value object '${f.ref}' has no descriptor`);
487
+ return sampleOfDescriptor(dep, byName, includeOptional, path);
414
488
  }
415
489
  }
416
490
  }
417
491
 
418
- function sampleOfDescriptor(d: ValueObjectDescriptor, byName: Map<string, ValueObjectDescriptor>, includeOptional = false): unknown {
492
+ function sampleOfDescriptor(
493
+ d: ValueObjectDescriptor,
494
+ byName: Map<string, ValueObjectDescriptor>,
495
+ includeOptional = false,
496
+ path = d.name,
497
+ ): unknown {
419
498
  if (d.geometry !== undefined) return GEOMETRY_SAMPLES[d.geometry];
420
499
  if (d.union !== undefined) {
421
500
  // FIRST declared arm (keys are sorted — deterministic); `example:` overrides upstream.
422
501
  const [key, arm] = Object.entries(d.union)[0]!;
423
502
  if ("ref" in arm) {
424
503
  const dep = byName.get(arm.ref);
425
- if (dep === undefined) throw new Error(`voSample: union arm VO '${arm.ref}' has no descriptor`);
426
- return { type: key, value: sampleOfDescriptor(dep, byName) };
504
+ if (dep === undefined) throw new Error(`voSample: field '${path}' — union arm VO '${arm.ref}' has no descriptor`);
505
+ return { type: key, value: sampleOfDescriptor(dep, byName, includeOptional, `${path}.value`) };
427
506
  }
428
507
  const out: Record<string, unknown> = { type: key };
429
508
  for (const [k, f] of Object.entries(arm.fields)) {
430
509
  if (f.optional === true && !includeOptional) continue;
431
- out[k] = sampleOfDescriptorField(f, k, byName, includeOptional);
510
+ out[k] = sampleOfDescriptorField(f, k, byName, includeOptional, `${path}.${k}`);
432
511
  }
433
512
  return out;
434
513
  }
435
514
  const out: Record<string, unknown> = {};
436
515
  for (const [k, f] of Object.entries(d.fields ?? {})) {
437
516
  if (f.optional === true && !includeOptional) continue; // optional → omitted (mirrors payload synthesis)
438
- out[k] = sampleOfDescriptorField(f, k, byName, includeOptional);
517
+ out[k] = sampleOfDescriptorField(f, k, byName, includeOptional, `${path}.${k}`);
439
518
  }
440
519
  return out;
441
520
  }
@@ -446,12 +525,14 @@ function sampleOfDescriptor(d: ValueObjectDescriptor, byName: Map<string, ValueO
446
525
  * VOs). Proof synthesis calls this instead of guessing at the raw zod — which is exactly the
447
526
  * lane that refused its own sample on constrained shapes (geo min-2 coordinates).
448
527
  */
449
- export function voSample(meta: ValueObjectFieldMeta, opts?: { includeOptional?: boolean }): unknown {
528
+ export function voSample(meta: ValueObjectFieldMeta, opts?: { includeOptional?: boolean; path?: string }): unknown {
450
529
  if (meta.example !== undefined) return meta.example;
451
530
  const byName = new Map<string, ValueObjectDescriptor>();
452
531
  for (const d of meta.dependencies) byName.set(d.name, d);
453
532
  byName.set(meta.descriptor.name, meta.descriptor);
454
- return sampleOfDescriptor(meta.descriptor, byName, opts?.includeOptional === true);
533
+ // `path` (when the caller threads the payload-root field path) makes any interior failure name the
534
+ // FULL path (`georef.controlPoints[0].geoPoint`), not a bare VO name — the exact-field-path contract.
535
+ return sampleOfDescriptor(meta.descriptor, byName, opts?.includeOptional === true, opts?.path ?? meta.descriptor.name);
455
536
  }
456
537
 
457
538
  function makeValueObject<T>(
@@ -692,6 +773,11 @@ export interface GeoPolygonValue {
692
773
  type: "Polygon";
693
774
  coordinates: number[][][];
694
775
  }
776
+ /** The authored TS shape of a `geo.multiPoint(...)` value (RFC 7946 MultiPoint). */
777
+ export interface GeoMultiPointValue {
778
+ type: "MultiPoint";
779
+ coordinates: number[][];
780
+ }
695
781
 
696
782
  function geoZod(kind: GeometryKind, coordinates: z.ZodTypeAny): z.ZodTypeAny {
697
783
  return z.object({ type: z.literal(kind), coordinates });
@@ -739,6 +825,18 @@ export const geo = {
739
825
  "Polygon",
740
826
  );
741
827
  },
828
+ /** An RFC 7946 **MultiPoint** (an array of positions). Round-trips as typed geometry;
829
+ * NOT yet spatial-indexable (the R*Tree is point-only — non-point indexing is future
830
+ * kernel work). */
831
+ multiPoint(name = "GeoMultiPoint"): ValueObject<GeoMultiPointValue> {
832
+ return makeValueObject<GeoMultiPointValue>(
833
+ name,
834
+ geoZod("MultiPoint", z.array(positionZod)) as z.ZodType<GeoMultiPointValue>,
835
+ { name, geometry: "MultiPoint" },
836
+ [],
837
+ "MultiPoint",
838
+ );
839
+ },
742
840
  };
743
841
 
744
842
  // ── module-plane collection (composeDomainModule / codegen) ──────────────────────────
@@ -795,7 +893,9 @@ export function collectValueObjectDescriptors(
795
893
  const byName = new Map<string, ValueObjectDescriptor>();
796
894
  for (const agg of aggregates) {
797
895
  for (const field of Object.values(agg.fields)) {
798
- const meta = field.valueObject;
896
+ // A VO can ride a field directly (`VO.field()`) OR as a map's value
897
+ // (`t.map(VO.field())` → `field.mapValueVo`); collect the descriptor set of both.
898
+ for (const meta of [field.valueObject, field.mapValueVo]) {
799
899
  if (meta === undefined) continue;
800
900
  const all = [meta.descriptor, ...meta.dependencies];
801
901
  for (const d of all) {
@@ -808,6 +908,7 @@ export function collectValueObjectDescriptors(
808
908
  }
809
909
  byName.set(d.name, d);
810
910
  }
911
+ }
811
912
  }
812
913
  }
813
914
  return [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
@@ -77,8 +77,18 @@ export function encodeKernelSchema(agg: AggregateHandle): WireSchema {
77
77
  return out;
78
78
  }
79
79
 
80
- /** Map an authored op value onto the kernel `Value` shape, given the field kind. */
81
- function encodeKernelValue(field: Field | undefined, value: unknown): WireValue {
80
+ /** A shape description for a value that failed to encode (diagnostics only). */
81
+ function describeShape(value: unknown): string {
82
+ if (value === null) return "null";
83
+ if (Array.isArray(value)) return "array";
84
+ if (typeof value === "object") return "anonymous object";
85
+ return typeof value;
86
+ }
87
+
88
+ /** Map an authored op value onto the kernel `Value` shape, given the field kind.
89
+ * `op` is passed purely for a NAMED diagnostic (aggregate + field + expected VO type) when a value
90
+ * can't be lowered — never `field '?'`. */
91
+ function encodeKernelValue(field: Field | undefined, value: unknown, op?: FieldOp): WireValue {
82
92
  // An `evidence` field encodes as the FIRST-CLASS kernel `Value::Evidence` (NOT a JSON
83
93
  // string): the authored {hash, mediaType, byteLength, storageClass} maps to the
84
94
  // kernel's snake_case serde shape + the `Git`/`External` unit-variant. Checked BEFORE
@@ -100,15 +110,49 @@ function encodeKernelValue(field: Field | undefined, value: unknown): WireValue
100
110
  // (the kernel canonicalises it at the boundary). This is the new-era encoding; a package compiled
101
111
  // before this change froze the old `{ Str: JSON.stringify(value) }` lump and replays unchanged.
102
112
  if (field?.kind === "json") return { Json: value };
113
+ // A `t.map(t.json())` / `t.map(VO.field())` ENTRY value is a json leaf too. Its wire DRIVER is
114
+ // `MapOf(Lww)` (whose leaves are ordinary kernel `Value`s), so a json-valued entry lowers to the SAME
115
+ // first-class `Value::Json` a top-level json field does — the map entry round-trips as its OBJECT, not a
116
+ // JSON-shaped `Str`. Checked BEFORE the scalar fallthroughs. SAME-ERA treatment as the top-level json
117
+ // field (the new-era compiler emits Json unconditionally; an older bundle froze its own lump). A
118
+ // string/int-valued map (`mapValueKind !== "json"`) is UNTOUCHED here → its entries stay Str/Int,
119
+ // byte-identical. `mapValueKind === "json"` is set for BOTH `t.map(t.json())` and a VO-valued map
120
+ // (`t.map(VO.field())`, whose inner field is json-kind).
121
+ // ERA GUARD (2026-07-04 regression fix): ONLY structured values (object/array) take the Json
122
+ // lane. A STRING entry into a json-kind map stays Value::Str — that is the historical wire
123
+ // shape for every pre-stringified leaf, most critically writeTuple's relation `subjects`
124
+ // entries, which the kernel's authz fold parses as Str. Routing strings to Json broke
125
+ // relation seating repo-wide (warrant_flip/va_core/relation-bootstrap all red). Objects were
126
+ // previously a REFUSAL here, so giving them the Json lane changes only the case that threw.
127
+ if (field?.kind === "map" && field.mapValueKind === "json" && typeof value === "object" && value !== null) {
128
+ return { Json: value };
129
+ }
103
130
  // For scalar Set/SetEntry the kernel expects Str or Int. Maps store leaves;
104
131
  // a map's entry value is a leaf scalar.
105
132
  if (typeof value === "number" && Number.isInteger(value)) return { Int: value };
106
133
  if (typeof value === "string") return { Str: value };
107
134
  if (typeof value === "boolean") return { Str: String(value) };
108
135
  // Enum values are strings; refs are id strings — both handled above.
136
+ // NAMED diagnostic (never `field '?'`): identify the aggregate + field + the shape we got, and — when the
137
+ // field metadata was UNAVAILABLE (a sibling write whose target schema didn't reach the encoder) — say so,
138
+ // since that is the classic cause of a VO/json object arriving at the scalar fallthrough.
139
+ const aggName = op?.aggregateType ?? op?.aggregateId ?? "?";
140
+ const fieldName = op?.field ?? "?";
141
+ if (field === undefined) {
142
+ throw new Error(
143
+ `sibling write ${aggName}.${fieldName} got ${describeShape(value)} but its field metadata was ` +
144
+ `unavailable, so it could not be encoded — declare the write against the target aggregate's handle ` +
145
+ `(instance/existing/ensure(TargetAgg, id)) so the field kind is known.`,
146
+ );
147
+ }
148
+ // Name the expected VO: a top-level VO field carries it on `valueObject`, a VO-valued MAP entry on
149
+ // `mapValueVo` (the inner value's VO metadata) — so a bad `t.map(VO.field())` entry names its VO too.
150
+ const expected = field.valueObject?.name ?? field.mapValueVo?.name;
151
+ const kindLabel = field.kind === "map" ? `map (${field.mapValueKind ?? "?"}-valued)` : field.kind;
109
152
  throw new Error(
110
- `Cannot encode value ${JSON.stringify(value)} for field '${field?.kind ?? "?"}': ` +
111
- `kernel scalars are Str | Int only.`,
153
+ `${aggName}.${fieldName} is a '${kindLabel}' field${expected ? ` (value object ${expected})` : ""}, ` +
154
+ `but got ${describeShape(value)} (${JSON.stringify(value)}) — kernel scalars are Str | Int only. ` +
155
+ `A structured value needs a json/value-object field; a set/map needs its additive write.`,
112
156
  );
113
157
  }
114
158
 
@@ -123,7 +167,7 @@ function encodeKernelFieldOp(op: FieldOp, field: Field | undefined): WireFieldOp
123
167
  wireOp =
124
168
  driver === "LastPosition"
125
169
  ? { SetPosition: String(op.value) }
126
- : { Set: encodeKernelValue(field, op.value) };
170
+ : { Set: encodeKernelValue(field, op.value, op) };
127
171
  break;
128
172
  case "addToSet":
129
173
  // On a remove-wins set the add grows the 2-phase set's `adds`; on AddWins it is the plain union.
@@ -149,7 +193,7 @@ function encodeKernelFieldOp(op: FieldOp, field: Field | undefined): WireFieldOp
149
193
  break;
150
194
  case "setEntry":
151
195
  wireOp = {
152
- SetEntry: { key: op.entryKey!, value: encodeKernelValue(field, op.value) },
196
+ SetEntry: { key: op.entryKey!, value: encodeKernelValue(field, op.value, op) },
153
197
  };
154
198
  break;
155
199
  }
@@ -294,14 +338,18 @@ export function executeDirectiveToIntent<P>(
294
338
  explicitMarker.set(op.aggregateId, encoded);
295
339
  }
296
340
  // Field kinds are known for ops targeting the directive's own aggregate — by
297
- // TYPE for a bound op (`aggregateType`), else by id for an unbound op. Ops
298
- // fanning out to siblings encode by value-kind (the `json` path needs the kind).
341
+ // TYPE for a bound op (`aggregateType`), else by id for an unbound op.
299
342
  const isOwnTarget =
300
343
  op.aggregateType !== undefined ? op.aggregateType === agg.id : op.aggregateId === agg.id;
301
344
  // A bucket is the own target if ANY of its ops target the directive's own aggregate.
302
345
  if (isOwnTarget) ownTarget.set(op.aggregateId, true);
303
346
  else if (!ownTarget.has(op.aggregateId)) ownTarget.set(op.aggregateId, false);
304
- const field = isOwnTarget ? fieldsOf[op.field] : undefined;
347
+ // Resolve the field's schema from the OP'S OWN TARGET aggregate, not the dispatch primary: a fanned-out
348
+ // sibling op carries its target's Field (`op.targetField`, captured at op-construction where the handle
349
+ // knew its schema). For an own-target op `op.targetField` IS `fieldsOf[op.field]` (same Field object), so
350
+ // this is byte-identical for every primary write; it only supplies metadata that was previously LOST on a
351
+ // sibling write (a VO/json value that used to throw, a RemoveWins/LastPosition driver that used to mis-lower).
352
+ const field = op.targetField ?? (isOwnTarget ? fieldsOf[op.field] : undefined);
305
353
  bucket.push(encodeKernelFieldOp(op, field));
306
354
  }
307
355