@githolon/dsl 0.32.0 → 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.
- package/dart/lib/src/errors.dart +105 -1
- package/dart/lib/src/webview_bridge.dart +6 -4
- package/dart/test/webview_bridge_test.dart +65 -0
- package/package.json +1 -1
- package/src/build_package.ts +25 -0
- package/src/codegen_dart.ts +347 -3
- package/src/codegen_ts.ts +74 -1
- package/src/compile_package_main.ts +26 -2
- package/src/fields.ts +84 -0
- package/src/index.ts +19 -0
- package/src/usd.ts +21 -0
- package/src/usd_layers.ts +11 -0
- package/src/value_object.ts +413 -0
package/src/fields.ts
CHANGED
|
@@ -72,6 +72,62 @@ export const evidenceRefSchema: z.ZodType<EvidenceRefValue> = z.object({
|
|
|
72
72
|
storageClass: z.enum(["git", "external"]),
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
// ── VALUE OBJECTS (composable value objects — `value_object.ts`) ─────────────────────
|
|
76
|
+
// The descriptor types live HERE (dependency-free) so `Field` can carry the metadata
|
|
77
|
+
// without a `fields.ts → value_object.ts` cycle; `value_object.ts` re-exports them.
|
|
78
|
+
|
|
79
|
+
/** The RFC 7946 geometry kinds a `geo.*` value object can declare (Point is the
|
|
80
|
+
* spatial-index-blessed one; see `spatial.ts`). */
|
|
81
|
+
export type GeometryKind = "Point" | "LineString" | "Polygon";
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* One field of a value-object DESCRIPTOR — the canonical, JSON-schema-ish shape that
|
|
85
|
+
* rides the package IR (`nomosValueObjects`) and drives TS/Dart codegen. `optional`
|
|
86
|
+
* is PRESENT only when true (omit-when-false — descriptor bytes are deterministic).
|
|
87
|
+
* `array.items` is a nested descriptor WITHOUT `optional` (an array of optionals is
|
|
88
|
+
* not expressible — the v1 constrained set).
|
|
89
|
+
*/
|
|
90
|
+
export type ValueObjectFieldDescriptor =
|
|
91
|
+
| { readonly type: "string"; readonly optional?: true }
|
|
92
|
+
| { readonly type: "number"; readonly optional?: true }
|
|
93
|
+
| { readonly type: "boolean"; readonly optional?: true }
|
|
94
|
+
| { readonly type: "enum"; readonly values: readonly string[]; readonly optional?: true }
|
|
95
|
+
| { readonly type: "array"; readonly items: ValueObjectFieldDescriptor; readonly optional?: true }
|
|
96
|
+
/** A nested value object, referenced BY NAME (its own descriptor rides alongside). */
|
|
97
|
+
| { readonly type: "object"; readonly ref: string; readonly optional?: true }
|
|
98
|
+
/** A nested GEO value object (RFC 7946 geometry), referenced by name. */
|
|
99
|
+
| { readonly type: "geometry"; readonly ref: string; readonly geometry: GeometryKind; readonly optional?: true };
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The canonical descriptor of ONE named value object. A structural VO carries
|
|
103
|
+
* `fields` (keys SORTED — byte-determinism); a `geo.*` VO carries `geometry`
|
|
104
|
+
* instead (its shape is fixed by RFC 7946, not authored).
|
|
105
|
+
*/
|
|
106
|
+
export interface ValueObjectDescriptor {
|
|
107
|
+
readonly name: string;
|
|
108
|
+
readonly fields?: Readonly<Record<string, ValueObjectFieldDescriptor>>;
|
|
109
|
+
readonly geometry?: GeometryKind;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The value-object metadata a VO-typed `Field` carries (`ValueObject.field()` sets it).
|
|
114
|
+
* FRONTEND/IR-ONLY: it never enters the kernel wire `Schema` (driver-only — see
|
|
115
|
+
* `encodeKernelSchema`) nor the canonical manifest, so a VO field's domainHash is
|
|
116
|
+
* IDENTICAL to a hand-rolled `t.jsonObject()` field. The additive `nomosValueObjects`
|
|
117
|
+
* USD key (built by `composeDomainModule`/`usd.ts` FROM this metadata) is what makes
|
|
118
|
+
* the structure visible to tooling.
|
|
119
|
+
*/
|
|
120
|
+
export interface ValueObjectFieldMeta {
|
|
121
|
+
/** The VO's declared name (the generated TS interface / Dart class name). */
|
|
122
|
+
readonly name: string;
|
|
123
|
+
/** This VO's own canonical descriptor. */
|
|
124
|
+
readonly descriptor: ValueObjectDescriptor;
|
|
125
|
+
/** Every TRANSITIVELY nested VO's descriptor (self excluded), sorted by name. */
|
|
126
|
+
readonly dependencies: readonly ValueObjectDescriptor[];
|
|
127
|
+
/** Present iff this VO is a `geo.*` geometry (the spatial-index pairing check). */
|
|
128
|
+
readonly geometry?: GeometryKind;
|
|
129
|
+
}
|
|
130
|
+
|
|
75
131
|
export interface Field<T = unknown, K extends FieldKind = FieldKind> {
|
|
76
132
|
/** The kind tag. `K` is the LITERAL kind so the query DSL can constrain `.ref`
|
|
77
133
|
* to ONLY `kind:'ref'` fields at compile time (`t.ref` → `Field<string,'ref'>`).
|
|
@@ -113,6 +169,15 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
|
|
|
113
169
|
* `encodeKernelSchema`) — so it changes NO hash (identical to `mapValueKind`/`isOptional`).
|
|
114
170
|
*/
|
|
115
171
|
readonly jsonShape?: "object";
|
|
172
|
+
/**
|
|
173
|
+
* For a `json`-kind field lowered from a NAMED VALUE OBJECT (`ValueObject.field()`):
|
|
174
|
+
* the VO metadata (name + canonical descriptor + nested-VO dependencies). Drives the
|
|
175
|
+
* generated TS interface / Dart class and the additive `nomosValueObjects` USD key.
|
|
176
|
+
* FRONTEND/IR-ONLY — never in the kernel wire `Schema` (driver-only), so the field's
|
|
177
|
+
* canonical-manifest bytes are identical to a plain `t.jsonObject()` (see
|
|
178
|
+
* {@link ValueObjectFieldMeta}). `undefined` for every non-VO field.
|
|
179
|
+
*/
|
|
180
|
+
readonly valueObject?: ValueObjectFieldMeta;
|
|
116
181
|
/**
|
|
117
182
|
* Whether a folded aggregate may LACK this field. A SCALAR field
|
|
118
183
|
* (`string`/`int`/`enum`/`ref`/`json`) that a `.creates` directive does not
|
|
@@ -184,6 +249,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
184
249
|
isOptional?: boolean;
|
|
185
250
|
mapValueKind?: FieldKind;
|
|
186
251
|
jsonShape?: "object";
|
|
252
|
+
valueObject?: ValueObjectFieldMeta;
|
|
187
253
|
encrypted?: true;
|
|
188
254
|
meta?: true;
|
|
189
255
|
facet?: FieldFacet;
|
|
@@ -201,6 +267,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
201
267
|
...(extra.viaField !== undefined ? { viaField: extra.viaField } : {}),
|
|
202
268
|
...(extra.mapValueKind !== undefined ? { mapValueKind: extra.mapValueKind } : {}),
|
|
203
269
|
...(extra.jsonShape !== undefined ? { jsonShape: extra.jsonShape } : {}),
|
|
270
|
+
...(extra.valueObject !== undefined ? { valueObject: extra.valueObject } : {}),
|
|
204
271
|
...(extra.encrypted ? { isEncrypted: true as const } : {}),
|
|
205
272
|
...(extra.meta ? { isMeta: true as const } : {}),
|
|
206
273
|
merge(d: Driver) {
|
|
@@ -401,3 +468,20 @@ function refImpl(
|
|
|
401
468
|
}
|
|
402
469
|
return makeField<string, "ref">("ref", z.string(), Conflict, { refAggregateId: a.id });
|
|
403
470
|
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* INTERNAL (`value_object.ts` — `ValueObject.field()`): construct the JSON-OBJECT value
|
|
474
|
+
* leaf a named value object lowers to. Storage/merge-identical to `t.jsonObject()`
|
|
475
|
+
* (kind `"json"`, `jsonShape:"object"`, driver Lww — the whole VO replaces WHOLESALE;
|
|
476
|
+
* per-subfield merge is what aggregates/maps are for), PLUS the {@link
|
|
477
|
+
* ValueObjectFieldMeta} that names the VO for codegen + the `nomosValueObjects` IR key.
|
|
478
|
+
* The zod is the VO's TYPED object schema, so `set(handle, field, value)` and payload
|
|
479
|
+
* embedding validate the real structure. Not part of `t.*` — reach it via
|
|
480
|
+
* `valueObject(...)`/`geo.*`'s `.field()`.
|
|
481
|
+
*/
|
|
482
|
+
export function valueObjectField<T>(
|
|
483
|
+
zod: z.ZodType<T>,
|
|
484
|
+
valueObject: ValueObjectFieldMeta,
|
|
485
|
+
): Field<T, "json"> {
|
|
486
|
+
return makeField<T, "json">("json", zod, Lww, { jsonShape: "object", valueObject });
|
|
487
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -40,6 +40,25 @@ export {
|
|
|
40
40
|
secretField,
|
|
41
41
|
publicField,
|
|
42
42
|
} from "./fields.js";
|
|
43
|
+
// FIRST-CLASS COMPOSABLE VALUE OBJECTS (`value_object.ts`): named, reusable, nestable
|
|
44
|
+
// structured values — ONE definition yields the zod (payload validation), the TS type,
|
|
45
|
+
// the generated Dart class, and the `nomosValueObjects` IR descriptor. A VO lowers to a
|
|
46
|
+
// single Lww-class json-object leaf (wholesale-replace merge — no per-subfield merge).
|
|
47
|
+
// `geo.*` are the RFC 7946 geometry VOs (pair `geo.point()` with `spatial(...)`).
|
|
48
|
+
export {
|
|
49
|
+
valueObject,
|
|
50
|
+
geo,
|
|
51
|
+
type ValueObject,
|
|
52
|
+
type ValueObjectValue,
|
|
53
|
+
type OptionalValueObject,
|
|
54
|
+
type ValueObjectShapeEntry,
|
|
55
|
+
type ValueObjectDescriptor,
|
|
56
|
+
type ValueObjectFieldDescriptor,
|
|
57
|
+
type GeometryKind,
|
|
58
|
+
type GeoPointValue,
|
|
59
|
+
type GeoLineStringValue,
|
|
60
|
+
type GeoPolygonValue,
|
|
61
|
+
} from "./value_object.js";
|
|
43
62
|
export {
|
|
44
63
|
type PrincipalEvidence,
|
|
45
64
|
type AttributionPrincipal,
|
package/src/usd.ts
CHANGED
|
@@ -464,6 +464,19 @@ export interface UsdLayer {
|
|
|
464
464
|
readonly reason: string | null;
|
|
465
465
|
}[];
|
|
466
466
|
};
|
|
467
|
+
/**
|
|
468
|
+
* FIRST-CLASS VALUE OBJECTS (`value_object.ts`) — the canonical descriptors of every
|
|
469
|
+
* named value object this layer's aggregates carry (deduped, SORTED by name, nested
|
|
470
|
+
* VOs included), so tooling/visualizers SEE the structure a VO field stores. PURELY
|
|
471
|
+
* DESCRIPTIVE: the kernel reads no gate off it (a VO field folds exactly like a
|
|
472
|
+
* `t.jsonObject()` leaf — one wholesale-replace json value). OMITTED for VO-free law
|
|
473
|
+
* (byte-identical hash — the era rule); declaring a VO moves the PACKAGE hash only
|
|
474
|
+
* (the canonical manifest/domainHash is untouched — the VO is not wire law).
|
|
475
|
+
*/
|
|
476
|
+
readonly nomosValueObjects?: {
|
|
477
|
+
readonly v: 1;
|
|
478
|
+
readonly valueObjects: readonly import("./fields.js").ValueObjectDescriptor[];
|
|
479
|
+
};
|
|
467
480
|
}
|
|
468
481
|
|
|
469
482
|
/** The reserved variant name declaring a set's DEFAULT (USD has a default variant). */
|
|
@@ -995,6 +1008,13 @@ export function emitUsd(
|
|
|
995
1008
|
// conversion's declared flow {directiveId, sink, facet, reason}. Hashed into nomosFlowGate +
|
|
996
1009
|
// witnessed fail-closed at runtime (Slice B). Omit-when-empty (a principal-free domain is
|
|
997
1010
|
// byte-identical). SORTED for determinism.
|
|
1011
|
+
// FIRST-CLASS VALUE OBJECTS: the module's collected descriptors (composeDomainModule
|
|
1012
|
+
// dedupes + sorts them). Omit-when-empty (era rule — VO-free law is byte-identical).
|
|
1013
|
+
const moduleValueObjects = (l.module as { valueObjects?: readonly import("./fields.js").ValueObjectDescriptor[] }).valueObjects;
|
|
1014
|
+
const nomosValueObjects =
|
|
1015
|
+
moduleValueObjects !== undefined && moduleValueObjects.length > 0
|
|
1016
|
+
? { v: 1 as const, valueObjects: [...moduleValueObjects] }
|
|
1017
|
+
: undefined;
|
|
998
1018
|
const flowEndorse = [...l.module.directives]
|
|
999
1019
|
.flatMap((d) => harvestPrincipalFlows(d as never).map((f) => ({ directiveId: d.id, sink: f.sink, facet: f.facet, reason: f.reason })))
|
|
1000
1020
|
.sort((a, b) =>
|
|
@@ -1040,6 +1060,7 @@ export function emitUsd(
|
|
|
1040
1060
|
...(nomosBirthGate !== undefined ? { nomosBirthGate } : {}),
|
|
1041
1061
|
...(nomosParentAttestedBirth !== undefined ? { nomosParentAttestedBirth } : {}),
|
|
1042
1062
|
...(nomosVerifiedFirstEnroll !== undefined ? { nomosVerifiedFirstEnroll } : {}),
|
|
1063
|
+
...(nomosValueObjects !== undefined ? { nomosValueObjects } : {}),
|
|
1043
1064
|
...(Object.keys(visAggregates).length > 0
|
|
1044
1065
|
? { nomosReadVisibility: { v: 1 as const, aggregates: visAggregates } }
|
|
1045
1066
|
: {}),
|
package/src/usd_layers.ts
CHANGED
|
@@ -190,6 +190,9 @@ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string):
|
|
|
190
190
|
...(layer.nomosVerifiedFirstEnroll !== undefined
|
|
191
191
|
? [`${indent}custom string nomos:verifiedFirstEnrollHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosVerifiedFirstEnroll)))}`]
|
|
192
192
|
: []),
|
|
193
|
+
...(layer.nomosValueObjects !== undefined
|
|
194
|
+
? [`${indent}custom string nomos:valueObjectsHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosValueObjects)))}`]
|
|
195
|
+
: []),
|
|
193
196
|
];
|
|
194
197
|
}
|
|
195
198
|
|
|
@@ -997,6 +1000,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
997
1000
|
const birthGateByDomain = new Map<string, UsdLayer["nomosBirthGate"]>();
|
|
998
1001
|
const parentAttestedBirthByDomain = new Map<string, UsdLayer["nomosParentAttestedBirth"]>();
|
|
999
1002
|
const verifiedFirstEnrollByDomain = new Map<string, UsdLayer["nomosVerifiedFirstEnroll"]>();
|
|
1003
|
+
const valueObjectsByDomain = new Map<string, UsdLayer["nomosValueObjects"]>();
|
|
1000
1004
|
for (const p of prims) {
|
|
1001
1005
|
if (p.attrs.get("nomos:kind") === "domain") {
|
|
1002
1006
|
const domain = attrString(p, "nomos:domain");
|
|
@@ -1068,6 +1072,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1068
1072
|
if (typeof vfeHex === "string") {
|
|
1069
1073
|
verifiedFirstEnrollByDomain.set(domain, JSON.parse(unhexUtf8(vfeHex)) as UsdLayer["nomosVerifiedFirstEnroll"]);
|
|
1070
1074
|
}
|
|
1075
|
+
// FIRST-CLASS VALUE OBJECTS: the hex-carried descriptor set (descriptive, not a gate).
|
|
1076
|
+
const voHex = p.attrs.get("nomos:valueObjectsHex");
|
|
1077
|
+
if (typeof voHex === "string") {
|
|
1078
|
+
valueObjectsByDomain.set(domain, JSON.parse(unhexUtf8(voHex)) as UsdLayer["nomosValueObjects"]);
|
|
1079
|
+
}
|
|
1071
1080
|
}
|
|
1072
1081
|
}
|
|
1073
1082
|
|
|
@@ -1169,6 +1178,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1169
1178
|
const birthGate = birthGateByDomain.get(domain);
|
|
1170
1179
|
const parentAttestedBirth = parentAttestedBirthByDomain.get(domain);
|
|
1171
1180
|
const verifiedFirstEnroll = verifiedFirstEnrollByDomain.get(domain);
|
|
1181
|
+
const valueObjects = valueObjectsByDomain.get(domain);
|
|
1172
1182
|
return {
|
|
1173
1183
|
path: `/Nomos/${domain}`,
|
|
1174
1184
|
prims: [...b.prims].sort(byPath),
|
|
@@ -1189,6 +1199,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1189
1199
|
...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
|
|
1190
1200
|
...(parentAttestedBirth !== undefined ? { nomosParentAttestedBirth: parentAttestedBirth } : {}),
|
|
1191
1201
|
...(verifiedFirstEnroll !== undefined ? { nomosVerifiedFirstEnroll: verifiedFirstEnroll } : {}),
|
|
1202
|
+
...(valueObjects !== undefined ? { nomosValueObjects: valueObjects } : {}),
|
|
1192
1203
|
};
|
|
1193
1204
|
})
|
|
1194
1205
|
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
@@ -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
|
+
}
|