@githolon/dsl 0.38.0 → 0.39.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/package.json +1 -1
- package/src/codegen_dart.ts +32 -6
- package/src/codegen_proof.ts +7 -0
- package/src/codegen_provider_dart.ts +11 -0
- package/src/codegen_ts.ts +14 -1
- package/src/fields.ts +3 -0
- package/src/index.ts +3 -0
- package/src/shape_compat.ts +5 -0
- package/src/value_object.ts +143 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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_dart.ts
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* generated, so they stay reproducible from the domain definitions.
|
|
30
30
|
*/
|
|
31
31
|
import { z } from "zod";
|
|
32
|
+
import { voMetaOf, collectPayloadValueObjectDescriptors } from "./value_object.js";
|
|
32
33
|
import type { AggregateHandle } from "./aggregate.js";
|
|
33
34
|
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
34
35
|
import type { RelationTypeDecl } from "./relation_schema.js";
|
|
@@ -60,7 +61,9 @@ export type ToJsonWrap =
|
|
|
60
61
|
| "scalar" // String/int/bool — emit the field directly.
|
|
61
62
|
| "enum" // generated enum — emit `.wire`.
|
|
62
63
|
| "strList" // List<String> — emit directly.
|
|
63
|
-
| "json"
|
|
64
|
+
| "json" // nested object — emit the field's `Map<String, Object?>` directly.
|
|
65
|
+
| "vo" // a declared value object — emit `.toJson()` (the generated VO class).
|
|
66
|
+
| "voList"; // a list of value objects — emit `.map((e) => e.toJson()).toList()`.
|
|
64
67
|
|
|
65
68
|
export interface PayloadFieldSpec {
|
|
66
69
|
name: string;
|
|
@@ -262,19 +265,31 @@ function introspectField(
|
|
|
262
265
|
}
|
|
263
266
|
case "ZodArray": {
|
|
264
267
|
const elem = zodArrayElement(zt);
|
|
265
|
-
// A `string[]` is a typed `List<String>`.
|
|
266
|
-
//
|
|
267
|
-
// JSON DATA to the engine, modelled as `List<Object
|
|
268
|
+
// A `string[]` is a typed `List<String>`. A list of DECLARED VALUE OBJECTS is typed
|
|
269
|
+
// with the generated VO class (vo_authoring_end_to_end.md slice B). ANY other element
|
|
270
|
+
// type is arbitrary JSON DATA to the engine, modelled as `List<Object?>`.
|
|
268
271
|
if (zodTypeName(elem) === "ZodString") {
|
|
269
272
|
return { name, dartType: "List<String>", toJson: "strList" };
|
|
270
273
|
}
|
|
274
|
+
const elemVo = voMetaOf(elem);
|
|
275
|
+
if (elemVo !== undefined) {
|
|
276
|
+
return { name, dartType: `List<${elemVo.name}>`, toJson: "voList" };
|
|
277
|
+
}
|
|
271
278
|
return { name, dartType: "List<Object?>", toJson: "json" };
|
|
272
279
|
}
|
|
273
|
-
case "ZodObject":
|
|
280
|
+
case "ZodObject": {
|
|
281
|
+
// A DECLARED VALUE OBJECT keeps its name: the payload field is the generated VO class
|
|
282
|
+
// (typed author path — the same class the read model decodes to), serialised via its
|
|
283
|
+
// own `toJson()`. An anonymous `z.object(...)` stays the raw-map lane below.
|
|
284
|
+
const vo = voMetaOf(zt);
|
|
285
|
+
if (vo !== undefined) {
|
|
286
|
+
return { name, dartType: vo.name, toJson: "vo" };
|
|
287
|
+
}
|
|
274
288
|
// Nested object payload (e.g. `target: z.object({targetType, targetId})`). The
|
|
275
289
|
// engine receives it as DATA verbatim, so we model it as an arbitrary JSON map
|
|
276
290
|
// — the call site builds the literal, and `toPayloadJson()` ships it through.
|
|
277
291
|
return { name, dartType: "Map<String, Object?>", toJson: "json" };
|
|
292
|
+
}
|
|
278
293
|
case "ZodRecord": {
|
|
279
294
|
const valueType = zodRecordValueType(zt);
|
|
280
295
|
const dartType =
|
|
@@ -1908,6 +1923,10 @@ export function toJsonExpr(spec: PayloadFieldSpec, accessor: string): string {
|
|
|
1908
1923
|
return accessor;
|
|
1909
1924
|
case "enum":
|
|
1910
1925
|
return `${accessor}.wire`;
|
|
1926
|
+
case "vo":
|
|
1927
|
+
return `${accessor}.toJson()`;
|
|
1928
|
+
case "voList":
|
|
1929
|
+
return `${accessor}.map((e) => e.toJson()).toList()`;
|
|
1911
1930
|
}
|
|
1912
1931
|
}
|
|
1913
1932
|
|
|
@@ -2942,7 +2961,14 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
|
|
|
2942
2961
|
const skipValueObjectNames = opts.skipValueObjectNames ?? new Set<string>();
|
|
2943
2962
|
// VALUE OBJECTS: one generated class per named VO the module carries (composeDomainModule
|
|
2944
2963
|
// collected + sorted them), minus the package-shared ones (emitted once in the framework file).
|
|
2945
|
-
|
|
2964
|
+
// Payload-only VOs (a VO used in a directive payload but on no aggregate field) still need
|
|
2965
|
+
// their generated class — collected from the branded payload schemas, deduped by name against
|
|
2966
|
+
// the aggregate-derived set. Codegen-only: the IR key stays aggregate-derived (era rule).
|
|
2967
|
+
const aggVoNames = new Set((mod.valueObjects ?? []).map((d) => d.name));
|
|
2968
|
+
const payloadVos = collectPayloadValueObjectDescriptors(mod.directives ?? []).filter((d) => !aggVoNames.has(d.name));
|
|
2969
|
+
const localValueObjects = [...(mod.valueObjects ?? []), ...payloadVos]
|
|
2970
|
+
.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
|
|
2971
|
+
.filter((d) => !skipValueObjectNames.has(d.name));
|
|
2946
2972
|
// Each VO's block = its FIELD ENUMS (real Dart enums, tolerant fromWire) then its class —
|
|
2947
2973
|
// so skipping a shared VO (the framework-file dedup) skips its enums with it.
|
|
2948
2974
|
const voBlock =
|
package/src/codegen_proof.ts
CHANGED
|
@@ -44,6 +44,7 @@ import type { FieldOp } from "./ops.js";
|
|
|
44
44
|
import type { RelationTypeDecl, Rewrite } from "./relation_schema.js";
|
|
45
45
|
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
46
46
|
import { finishCount } from "./count.js";
|
|
47
|
+
import { voMetaOf, voSample } from "./value_object.js";
|
|
47
48
|
import { mintedCreateField } from "./workspace_routing.js";
|
|
48
49
|
import { deterministicPorts } from "./ctx.js";
|
|
49
50
|
import { __setDispatchDomain } from "./framework/relations.js";
|
|
@@ -98,6 +99,12 @@ function sampleString(zt: z.ZodTypeAny, field: string): string {
|
|
|
98
99
|
|
|
99
100
|
/** One sample value for one zod shape. NO FALLBACK: an unhandled shape throws, naming its remedy. */
|
|
100
101
|
function sampleOfZod(zt: z.ZodTypeAny, field: string, where: string): unknown {
|
|
102
|
+
// VALUE OBJECTS FIRST (vo_authoring_end_to_end.md slice C): a declared VO carries its own
|
|
103
|
+
// lawful sample (the author's validated `example`, the blessed geometry sample, or per-field
|
|
104
|
+
// descriptor synthesis) — the raw-zod walk below cannot see constraints like geo's min-2
|
|
105
|
+
// coordinates and would synthesize a value the law itself refuses.
|
|
106
|
+
const vo = voMetaOf(zt);
|
|
107
|
+
if (vo !== undefined) return voSample(vo);
|
|
101
108
|
switch (zodKind(zt)) {
|
|
102
109
|
case "string":
|
|
103
110
|
return sampleString(zt, field);
|
|
@@ -171,6 +171,17 @@ function obligationFromJsonExpr(s: PayloadFieldSpec): string {
|
|
|
171
171
|
// a list. The Dart type drives the cast.
|
|
172
172
|
return s.optional ? `${raw} as ${s.dartType}?` : `${raw} as ${s.dartType}`;
|
|
173
173
|
}
|
|
174
|
+
case "vo": {
|
|
175
|
+
// A declared value object — decode through the generated class (typed both ways).
|
|
176
|
+
return s.optional
|
|
177
|
+
? `${raw} == null ? null : ${s.dartType}.fromProjected((${raw} as Map).cast<String, Object?>())`
|
|
178
|
+
: `${s.dartType}.fromProjected((${raw} as Map).cast<String, Object?>())`;
|
|
179
|
+
}
|
|
180
|
+
case "voList": {
|
|
181
|
+
const elem = s.dartType.replace(/^List<(.*)>$/, "$1");
|
|
182
|
+
const decode = `(${raw} as List).map((e) => ${elem}.fromProjected((e as Map).cast<String, Object?>())).toList()`;
|
|
183
|
+
return s.optional ? `${raw} == null ? null : ${decode}` : decode;
|
|
184
|
+
}
|
|
174
185
|
}
|
|
175
186
|
}
|
|
176
187
|
|
package/src/codegen_ts.ts
CHANGED
|
@@ -41,6 +41,7 @@ import type { Field, ValueObjectDescriptor, ValueObjectFieldDescriptor } from ".
|
|
|
41
41
|
import type { Directive } from "./directive.js";
|
|
42
42
|
import type { DomainModule } from "./codegen_dart.js";
|
|
43
43
|
import type { QueryDecl } from "./query.js";
|
|
44
|
+
import { voMetaOf, collectPayloadValueObjectDescriptors } from "./value_object.js";
|
|
44
45
|
import { finishCount } from "./count.js";
|
|
45
46
|
import { finishSum } from "./sum.js";
|
|
46
47
|
import type { DerivedDecl } from "./derived.js";
|
|
@@ -122,6 +123,14 @@ const paren = (t: string) => (t.includes("|") || t.includes("&") ? `(${t})` : t)
|
|
|
122
123
|
|
|
123
124
|
/** Recursive zod → TS type text. NO FALLBACK: an unhandled shape throws. */
|
|
124
125
|
function tsTypeOfZod(zt: z.ZodTypeAny, indent: string): string {
|
|
126
|
+
// A DECLARED VALUE OBJECT keeps its NAME (vo_authoring_end_to_end.md slice B): the payload
|
|
127
|
+
// references the same exported interface the read model uses — one name, both directions —
|
|
128
|
+
// instead of an anonymous inline literal type. Checked on the BARE object only: optional/
|
|
129
|
+
// default wrappers must keep flowing through the switch so `| undefined` survives.
|
|
130
|
+
if (zodKind(zt) === "object") {
|
|
131
|
+
const vo = voMetaOf(zt);
|
|
132
|
+
if (vo !== undefined) return vo.name;
|
|
133
|
+
}
|
|
125
134
|
switch (zodKind(zt)) {
|
|
126
135
|
case "string":
|
|
127
136
|
return "string";
|
|
@@ -476,7 +485,11 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
476
485
|
// ── value objects: one interface per named VO, deduped across the package's domains ──
|
|
477
486
|
const voByName = new Map<string, ValueObjectDescriptor>();
|
|
478
487
|
for (const mod of modules) {
|
|
479
|
-
|
|
488
|
+
// aggregate-carried VOs ∪ payload-only VOs (branded schemas) — both need their interface.
|
|
489
|
+
const payloadVos = collectPayloadValueObjectDescriptors(
|
|
490
|
+
((mod as { directives?: readonly { payloadSchema?: unknown }[] }).directives ?? []),
|
|
491
|
+
);
|
|
492
|
+
for (const d of [...((mod as { valueObjects?: readonly ValueObjectDescriptor[] }).valueObjects ?? []), ...payloadVos]) {
|
|
480
493
|
const existing = voByName.get(d.name);
|
|
481
494
|
if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(d)) {
|
|
482
495
|
throw new Error(
|
package/src/fields.ts
CHANGED
|
@@ -126,6 +126,9 @@ export interface ValueObjectFieldMeta {
|
|
|
126
126
|
readonly dependencies: readonly ValueObjectDescriptor[];
|
|
127
127
|
/** Present iff this VO is a `geo.*` geometry (the spatial-index pairing check). */
|
|
128
128
|
readonly geometry?: GeometryKind;
|
|
129
|
+
/** The author-supplied lawful example (validated against the VO's own zod at declaration).
|
|
130
|
+
* Proof synthesis + docs prefer it over per-field synthesis. */
|
|
131
|
+
readonly example?: unknown;
|
|
129
132
|
}
|
|
130
133
|
|
|
131
134
|
export interface Field<T = unknown, K extends FieldKind = FieldKind> {
|
package/src/index.ts
CHANGED
package/src/shape_compat.ts
CHANGED
|
@@ -80,6 +80,11 @@ function payloadFieldKind(spec: PayloadFieldSpec): string {
|
|
|
80
80
|
return "set";
|
|
81
81
|
case "json":
|
|
82
82
|
return "json";
|
|
83
|
+
case "vo":
|
|
84
|
+
case "voList":
|
|
85
|
+
// Interface kinds stay at the base lane for now (slice D of vo_authoring_end_to_end.md
|
|
86
|
+
// upgrades these to a first-class "vo" kind) — a VO IS a json leaf structurally.
|
|
87
|
+
return "json";
|
|
83
88
|
case "scalar":
|
|
84
89
|
default:
|
|
85
90
|
switch (spec.dartType) {
|
package/src/value_object.ts
CHANGED
|
@@ -226,12 +226,93 @@ function describeZod(voName: string, path: string, schema: z.ZodTypeAny): ValueO
|
|
|
226
226
|
|
|
227
227
|
const VO_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
228
228
|
|
|
229
|
+
/**
|
|
230
|
+
* THE VO BRAND (slice A of vo_authoring_end_to_end.md) — `valueObject()` stamps its zod schema
|
|
231
|
+
* with the VO's {@link ValueObjectFieldMeta} under this symbol, COMPILE-TIME ONLY: the brand is
|
|
232
|
+
* a non-enumerable property, never serialized, never read by the engine's validation path, so a
|
|
233
|
+
* branded schema's package bytes (and domainHash) are byte-identical to an unbranded one.
|
|
234
|
+
* Every generated-surface introspector (Dart/TS payload gen, interface kinds, proof synthesis)
|
|
235
|
+
* consults {@link voMetaOf} BEFORE its raw zod-kind switch — the VO keeps its NAME on every
|
|
236
|
+
* surface instead of degrading to an anonymous json object.
|
|
237
|
+
*/
|
|
238
|
+
export const NOMOS_VO_BRAND: unique symbol = Symbol.for("nomos.vo");
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Resolve the value-object identity of a zod schema, unwrapping `.optional()` / `.default()`
|
|
242
|
+
* wrappers (the brand lives on the VO's own object schema). `undefined` for anything that is
|
|
243
|
+
* not a declared VO — callers then fall through to their raw-zod lane, so non-VO behaviour is
|
|
244
|
+
* untouched.
|
|
245
|
+
*/
|
|
246
|
+
export function voMetaOf(zt: z.ZodTypeAny): ValueObjectFieldMeta | undefined {
|
|
247
|
+
let cur: z.ZodTypeAny | undefined = zt;
|
|
248
|
+
for (let hops = 0; cur !== undefined && hops < 16; hops++) {
|
|
249
|
+
const branded = (cur as unknown as Record<symbol, unknown>)[NOMOS_VO_BRAND];
|
|
250
|
+
if (branded !== undefined) return branded as ValueObjectFieldMeta;
|
|
251
|
+
const kind = zodKind(cur);
|
|
252
|
+
if (kind !== "optional" && kind !== "default" && kind !== "nullable") return undefined;
|
|
253
|
+
cur = zodDef(cur).innerType;
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The blessed lawful sample per RFC 7946 geometry kind (proof synthesis + docs). */
|
|
259
|
+
const GEOMETRY_SAMPLES: Record<GeometryKind, unknown> = {
|
|
260
|
+
Point: { type: "Point", coordinates: [0, 0] },
|
|
261
|
+
LineString: { type: "LineString", coordinates: [[0, 0], [1, 1]] },
|
|
262
|
+
Polygon: { type: "Polygon", coordinates: [[[0, 0], [0, 1], [1, 1], [0, 0]]] },
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
function sampleOfDescriptorField(
|
|
266
|
+
f: ValueObjectFieldDescriptor,
|
|
267
|
+
fieldName: string,
|
|
268
|
+
byName: Map<string, ValueObjectDescriptor>,
|
|
269
|
+
): unknown {
|
|
270
|
+
switch (f.type) {
|
|
271
|
+
case "string": return `sample-${fieldName}`;
|
|
272
|
+
case "number": return 1;
|
|
273
|
+
case "boolean": return true;
|
|
274
|
+
case "enum": return f.values[0];
|
|
275
|
+
case "array": return [sampleOfDescriptorField(f.items, fieldName, byName)];
|
|
276
|
+
case "geometry": return GEOMETRY_SAMPLES[f.geometry];
|
|
277
|
+
case "object": {
|
|
278
|
+
const dep = byName.get(f.ref);
|
|
279
|
+
if (dep === undefined) throw new Error(`voSample: nested value object '${f.ref}' has no descriptor`);
|
|
280
|
+
return sampleOfDescriptor(dep, byName);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function sampleOfDescriptor(d: ValueObjectDescriptor, byName: Map<string, ValueObjectDescriptor>): unknown {
|
|
286
|
+
if (d.geometry !== undefined) return GEOMETRY_SAMPLES[d.geometry];
|
|
287
|
+
const out: Record<string, unknown> = {};
|
|
288
|
+
for (const [k, f] of Object.entries(d.fields ?? {})) {
|
|
289
|
+
if (f.optional === true) continue; // optional → omitted (mirrors payload synthesis)
|
|
290
|
+
out[k] = sampleOfDescriptorField(f, k, byName);
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* A LAWFUL sample value for a VO (slice C): the author's validated `example` when given, the
|
|
297
|
+
* blessed geometry sample for `geo.*` kinds, else per-field synthesis (recursing into nested
|
|
298
|
+
* VOs). Proof synthesis calls this instead of guessing at the raw zod — which is exactly the
|
|
299
|
+
* lane that refused its own sample on constrained shapes (geo min-2 coordinates).
|
|
300
|
+
*/
|
|
301
|
+
export function voSample(meta: ValueObjectFieldMeta): unknown {
|
|
302
|
+
if (meta.example !== undefined) return meta.example;
|
|
303
|
+
const byName = new Map<string, ValueObjectDescriptor>();
|
|
304
|
+
for (const d of meta.dependencies) byName.set(d.name, d);
|
|
305
|
+
byName.set(meta.descriptor.name, meta.descriptor);
|
|
306
|
+
return sampleOfDescriptor(meta.descriptor, byName);
|
|
307
|
+
}
|
|
308
|
+
|
|
229
309
|
function makeValueObject<T>(
|
|
230
310
|
name: string,
|
|
231
311
|
zodSchema: z.ZodType<T>,
|
|
232
312
|
descriptor: ValueObjectDescriptor,
|
|
233
313
|
dependencies: readonly ValueObjectDescriptor[],
|
|
234
314
|
geometry?: GeometryKind,
|
|
315
|
+
example?: T,
|
|
235
316
|
): ValueObject<T> {
|
|
236
317
|
if (!VO_NAME_RE.test(name)) {
|
|
237
318
|
throw new Error(
|
|
@@ -239,12 +320,25 @@ function makeValueObject<T>(
|
|
|
239
320
|
`generated TS interface / Dart class name.`,
|
|
240
321
|
);
|
|
241
322
|
}
|
|
323
|
+
if (example !== undefined) {
|
|
324
|
+
const v = zodSchema.safeParse(example);
|
|
325
|
+
if (!v.success) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`valueObject '${name}': the declared example does not satisfy the VO's own schema — ` +
|
|
328
|
+
`a lying example would poison proof synthesis. ${v.error.message}`,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
242
332
|
const meta: ValueObjectFieldMeta = {
|
|
243
333
|
name,
|
|
244
334
|
descriptor,
|
|
245
335
|
dependencies,
|
|
246
336
|
...(geometry !== undefined ? { geometry } : {}),
|
|
337
|
+
...(example !== undefined ? { example } : {}),
|
|
247
338
|
};
|
|
339
|
+
// THE BRAND (slice A): compile-time identity on the schema itself — non-enumerable, never
|
|
340
|
+
// serialized, domainHash-inert. voMetaOf() is the one resolver every introspector uses.
|
|
341
|
+
Object.defineProperty(zodSchema, NOMOS_VO_BRAND, { value: meta, enumerable: false });
|
|
248
342
|
const vo: ValueObject<T> = {
|
|
249
343
|
__isValueObject: true,
|
|
250
344
|
name,
|
|
@@ -272,6 +366,13 @@ function makeValueObject<T>(
|
|
|
272
366
|
export function valueObject<const Name extends string, S extends Record<string, ValueObjectShapeEntry>>(
|
|
273
367
|
name: Name,
|
|
274
368
|
shape: S,
|
|
369
|
+
opts?: {
|
|
370
|
+
/** A lawful example value, VALIDATED against the VO's own schema at declaration (a lying
|
|
371
|
+
* example throws here, loudly). Proof synthesis prefers it over per-field synthesis — the
|
|
372
|
+
* escape hatch for constraint-rich shapes (refinements, cross-field rules) a synthesizer
|
|
373
|
+
* cannot guess. */
|
|
374
|
+
example?: ShapeValue<S>;
|
|
375
|
+
},
|
|
275
376
|
): ValueObject<ShapeValue<S>> {
|
|
276
377
|
const zodShape: Record<string, z.ZodTypeAny> = {};
|
|
277
378
|
const fields: Record<string, ValueObjectFieldDescriptor> = {};
|
|
@@ -308,6 +409,8 @@ export function valueObject<const Name extends string, S extends Record<string,
|
|
|
308
409
|
z.object(zodShape) as unknown as z.ZodType<ShapeValue<S>>,
|
|
309
410
|
descriptor,
|
|
310
411
|
dependencies,
|
|
412
|
+
undefined,
|
|
413
|
+
opts?.example,
|
|
311
414
|
);
|
|
312
415
|
}
|
|
313
416
|
|
|
@@ -388,6 +491,46 @@ export const geo = {
|
|
|
388
491
|
* descriptors throw (one name, one shape). Returns `[]` when no field carries a VO —
|
|
389
492
|
* the omit-when-empty input to the `nomosValueObjects` era key.
|
|
390
493
|
*/
|
|
494
|
+
/**
|
|
495
|
+
* Collect the value objects referenced by DIRECTIVE PAYLOADS (top-level fields + array
|
|
496
|
+
* elements), via the {@link NOMOS_VO_BRAND} the schemas carry. CODEGEN-ONLY (slice B of
|
|
497
|
+
* vo_authoring_end_to_end.md): the result feeds the generated Dart/TS surfaces so a
|
|
498
|
+
* payload-only VO still gets its class/interface — it does NOT enter the `nomosValueObjects`
|
|
499
|
+
* IR key (that stays aggregate-derived, keeping package bytes / domainHash untouched).
|
|
500
|
+
*/
|
|
501
|
+
export function collectPayloadValueObjectDescriptors(
|
|
502
|
+
directives: readonly { payloadSchema?: unknown }[],
|
|
503
|
+
): ValueObjectDescriptor[] {
|
|
504
|
+
const byName = new Map<string, ValueObjectDescriptor>();
|
|
505
|
+
const take = (meta: ValueObjectFieldMeta | undefined) => {
|
|
506
|
+
if (meta === undefined) return;
|
|
507
|
+
for (const d of [meta.descriptor, ...meta.dependencies]) {
|
|
508
|
+
const existing = byName.get(d.name);
|
|
509
|
+
if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(d)) {
|
|
510
|
+
throw new Error(`value object '${d.name}' has DIVERGENT shapes across payloads — one name, one shape.`);
|
|
511
|
+
}
|
|
512
|
+
byName.set(d.name, d);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
for (const dir of directives) {
|
|
516
|
+
const schema = dir?.payloadSchema as z.ZodTypeAny | undefined;
|
|
517
|
+
if (!schema || typeof schema !== "object" || zodKind(schema) !== "object") continue;
|
|
518
|
+
const rawShape = (zodDef(schema) as { shape?: Record<string, z.ZodTypeAny> | (() => Record<string, z.ZodTypeAny>) }).shape
|
|
519
|
+
?? (schema as unknown as { shape?: Record<string, z.ZodTypeAny> }).shape;
|
|
520
|
+
const shape = typeof rawShape === "function" ? rawShape() : rawShape ?? {};
|
|
521
|
+
for (const raw of Object.values(shape)) {
|
|
522
|
+
let zt = raw as z.ZodTypeAny;
|
|
523
|
+
take(voMetaOf(zt));
|
|
524
|
+
// unwrap optional/default to reach an array of VOs
|
|
525
|
+
for (let hops = 0; hops < 8 && (zodKind(zt) === "optional" || zodKind(zt) === "default"); hops++) {
|
|
526
|
+
zt = zodDef(zt).innerType!;
|
|
527
|
+
}
|
|
528
|
+
if (zodKind(zt) === "array") take(voMetaOf(zodArrayElement(zt)));
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
532
|
+
}
|
|
533
|
+
|
|
391
534
|
export function collectValueObjectDescriptors(
|
|
392
535
|
aggregates: readonly { id: string; fields: Record<string, Field> }[],
|
|
393
536
|
): ValueObjectDescriptor[] {
|