@githolon/dsl 0.52.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.
- package/compat_package.mjs +46 -0
- package/dart/lib/src/webview_bridge.dart +160 -13
- package/dart/pubspec.yaml +1 -1
- package/dart/test/co2_acceptance_test.dart +102 -0
- package/dart/test/compat_test.dart +61 -0
- package/dart/test/vo_read_typing_test.dart +113 -0
- package/package.json +2 -1
- package/src/authoring.ts +77 -17
- package/src/codegen_dart.ts +94 -12
- package/src/codegen_dart_client.ts +18 -1
- package/src/codegen_openapi.ts +1 -1
- package/src/codegen_proof.ts +113 -16
- package/src/codegen_ts.ts +87 -12
- package/src/compat_package_main.ts +187 -0
- package/src/compile_package_main.ts +11 -1
- package/src/fields.ts +30 -5
- package/src/index.ts +13 -0
- package/src/law_check.ts +132 -1
- package/src/ops.ts +25 -6
- package/src/path.ts +115 -0
- package/src/query.ts +18 -9
- package/src/shape_compat.ts +46 -0
- package/src/spatial.ts +8 -4
- package/src/value_object.ts +119 -18
- package/src/wire_encode.ts +57 -9
package/src/value_object.ts
CHANGED
|
@@ -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
|
|
48
|
-
* `z.string()`
|
|
49
|
-
* `z.
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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));
|
package/src/wire_encode.ts
CHANGED
|
@@ -77,8 +77,18 @@ export function encodeKernelSchema(agg: AggregateHandle): WireSchema {
|
|
|
77
77
|
return out;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
/**
|
|
81
|
-
function
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|