@githolon/dsl 0.53.0 → 0.54.1

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/src/authoring.ts CHANGED
@@ -24,8 +24,8 @@
24
24
  * So a `.plan` that fluently authors and `return []`s still emits its events — the recorded ops are real.
25
25
  */
26
26
  import type { AggregateHandle } from "./aggregate.js";
27
- import type { Field } from "./fields.js";
28
- import type { FieldOp, PlannedOp, PlannedEventMarker } from "./ops.js";
27
+ import type { Field, FieldValue } from "./fields.js";
28
+ import type { AcceptedValue, FieldOp, PlannedOp, PlannedEventMarker } from "./ops.js";
29
29
  import { endorsementOf } from "./principal_evidence.js";
30
30
  import { __qualifyAggregateType } from "./framework/relations.js";
31
31
  import { __registerEnsure } from "./attested_core.js";
@@ -52,14 +52,53 @@ function mint(typeTag: string): string {
52
52
  return (globalThis as unknown as { nomos: NomosMint }).nomos.mint(typeTag);
53
53
  }
54
54
 
55
- /** A reference to an aggregate the dev HOLDS — minted-new (`create`) or bound-to-existing (`existing`). */
56
- export interface AggregateRef {
55
+ /**
56
+ * `true` iff `F` is the WIDE, un-inferred field map (`Record<string, Field>`) — i.e. the ref
57
+ * came from an untyped handle. Used to DEGRADE the typed member signatures back to today's
58
+ * permissive `string`/`unknown` shape so every pre-existing plain-string call still compiles
59
+ * (the backward-compat contract: a typed handle catches typos, an untyped one is unchanged).
60
+ */
61
+ type UntypedFields<F> = string extends keyof F ? true : false;
62
+
63
+ /** Map-kind field keys of `F` (value type is a `Record<string, …>`) — the keys `.setEntry` accepts. */
64
+ type MapKeyOf<F extends Record<string, Field>> = {
65
+ [K in keyof F]: FieldValue<F[K]> extends Record<string, unknown> ? K : never;
66
+ }[keyof F] &
67
+ string;
68
+ /** The per-entry VALUE type of a map field `F[K]`. */
69
+ type MapValueOf<Fld> = FieldValue<Fld> extends Record<string, infer V> ? V : unknown;
70
+
71
+ /**
72
+ * A reference to an aggregate the dev HOLDS — minted-new (`create`) or bound-to-existing (`existing`).
73
+ *
74
+ * TYPED BY THE FIELD MAP (#29): when the source handle carries a concrete field map `F`
75
+ * (the usual `create(EstateDrawing)`), `.set("transform", value)` is checked by tsc — the
76
+ * field NAME must be a key of `F`, the VALUE type must match the field's declared kind, and a
77
+ * `t.set(...)` field REJECTS `.set` at the type level (the type-level twin of the runtime
78
+ * AddWins refusal — use `.add(...)`). An UNTYPED handle (`AggregateHandle` with the wide
79
+ * `Record<string, Field>`) degrades to the historical permissive `string`/`unknown` signature,
80
+ * so every plain-string authoring site compiles unchanged.
81
+ */
82
+ export interface AggregateRef<F extends Record<string, Field> = Record<string, Field>> {
57
83
  readonly __type: string;
58
84
  readonly __id: string;
59
- /** Set a scalar field (Lww). */
60
- set(field: string, value: unknown): this;
61
- /** Set one entry of a map field (per-key Lww). */
62
- setEntry(field: string, key: string, value: unknown): this;
85
+ /** Set a scalar/map-whole/ref field (Lww). Field name + value type are checked against `F`;
86
+ * a `t.set(...)` field is refused (use `.add`). */
87
+ set<K extends keyof F & string>(
88
+ field: UntypedFields<F> extends true
89
+ ? string
90
+ : K &
91
+ (F[K] extends { kind: "set" }
92
+ ? `TYPE ERROR: '${K}' is a set field — .set would OVERWRITE the merge; use .add("${K}", value)`
93
+ : K),
94
+ value: UntypedFields<F> extends true ? unknown : AcceptedValue<F[K]>,
95
+ ): this;
96
+ /** Set one entry of a map field (per-key Lww). On a typed handle the field must be a map field. */
97
+ setEntry<K extends MapKeyOf<F>>(
98
+ field: UntypedFields<F> extends true ? string : K,
99
+ key: string,
100
+ value: UntypedFields<F> extends true ? unknown : MapValueOf<F[K]>,
101
+ ): this;
63
102
  /** Add to a collection: a `t.hasMany` relation → writes the child's back-ref; a `t.set` → AddToSet on self. */
64
103
  add(field: string, value: unknown): this;
65
104
  /** Relate to another aggregate by reference (writes the ref field on THIS aggregate). */
@@ -87,7 +126,18 @@ class Ref implements AggregateRef {
87
126
  const resolved: Partial<FieldOp> = endorsement
88
127
  ? { ...extra, value: endorsement.principal, endorsement: { facet: endorsement.facet, reason: endorsement.reason } }
89
128
  : extra;
90
- const op: FieldOp = { aggregateId: this.__id, aggregateType: this.__type, field, kind, ...resolved };
129
+ // Capture the TARGET aggregate's Field so a fanned-out sibling write keeps its schema at wire encode
130
+ // (see FieldOp.targetField). The handle KNOWS its fields — thread that, don't re-resolve from the
131
+ // directive's primary aggregate. Absent field ⇒ no key (byte-identical to before).
132
+ const targetField = (this.handle.fields as Record<string, Field>)[field];
133
+ const op: FieldOp = {
134
+ aggregateId: this.__id,
135
+ aggregateType: this.__type,
136
+ ...(targetField ? { targetField } : {}),
137
+ field,
138
+ kind,
139
+ ...resolved,
140
+ };
91
141
  sink.push(this.marker ? { ...op, marker: this.marker } : op);
92
142
  }
93
143
  set(field: string, value: unknown): this {
@@ -121,16 +171,23 @@ class Ref implements AggregateRef {
121
171
  }
122
172
  }
123
173
 
124
- /** Ask Nomos to CREATE a new aggregate of the handle's type → a fluent reference (id minted, no choice). */
125
- export function create(agg: AggregateHandle): AggregateRef {
174
+ /** Ask Nomos to CREATE a new aggregate of the handle's type → a fluent reference (id minted, no choice).
175
+ * The returned ref is TYPED by the handle's field map `F` (see {@link AggregateRef}). */
176
+ export function create<Id extends string, F extends Record<string, Field>>(
177
+ agg: AggregateHandle<Id, F>,
178
+ ): AggregateRef<F> {
126
179
  // READ-PROJECTION NAMESPACING: mint under the QUALIFIED type tag — the kernel's id-mint gate
127
180
  // verifies a Create's id was minted for EXACTLY the declared (qualified) `__type`.
128
- return new Ref(agg, mint(__qualifyAggregateType(agg.id)), "creates");
181
+ return new Ref(agg, mint(__qualifyAggregateType(agg.id)), "creates") as unknown as AggregateRef<F>;
129
182
  }
130
183
 
131
- /** A reference to an EXISTING aggregate the dev already holds an id for (e.g. from the payload / a read). */
132
- export function existing(agg: AggregateHandle, id: string): AggregateRef {
133
- return new Ref(agg, id, null);
184
+ /** A reference to an EXISTING aggregate the dev already holds an id for (e.g. from the payload / a read).
185
+ * TYPED by the handle's field map `F`. */
186
+ export function existing<Id extends string, F extends Record<string, Field>>(
187
+ agg: AggregateHandle<Id, F>,
188
+ id: string,
189
+ ): AggregateRef<F> {
190
+ return new Ref(agg, id, null) as unknown as AggregateRef<F>;
134
191
  }
135
192
 
136
193
  /**
@@ -139,8 +196,11 @@ export function existing(agg: AggregateHandle, id: string): AggregateRef {
139
196
  * carries ONE referential marker for its primary aggregate; the SECOND+ aggregate it touches declares its
140
197
  * own birth through this (the fluent sibling of the typed `withMarker(set(instance(Agg, id), …), "ensures")`).
141
198
  */
142
- export function ensure(agg: AggregateHandle, id: string): AggregateRef {
143
- return new Ref(agg, id, "ensures");
199
+ export function ensure<Id extends string, F extends Record<string, Field>>(
200
+ agg: AggregateHandle<Id, F>,
201
+ id: string,
202
+ ): AggregateRef<F> {
203
+ return new Ref(agg, id, "ensures") as unknown as AggregateRef<F>;
144
204
  }
145
205
 
146
206
  // Register ensure() for the foreign read lane (attested reads — see attested_core.ts; the
@@ -29,7 +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, idOfMetaOf, NOMOS_UNIT_BRAND } from "./value_object.js";
32
+ import { voMetaOf, collectPayloadValueObjectDescriptors, idOfMetaOf, brandedIdMetaOf, NOMOS_UNIT_BRAND } from "./value_object.js";
33
33
  import type { AggregateHandle } from "./aggregate.js";
34
34
  import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
35
35
  import type { RelationTypeDecl } from "./relation_schema.js";
@@ -257,6 +257,11 @@ function introspectField(
257
257
  if (idAgg !== undefined) {
258
258
  return { name, dartType: typedIdClassForAggregateType(idAgg), toJson: "wrapped" };
259
259
  }
260
+ // brandedId("SiteId"): a standalone nominal string → the generated extension type.
261
+ const brand = brandedIdMetaOf(zt);
262
+ if (brand !== undefined) {
263
+ return { name, dartType: brand, toJson: "wrapped" };
264
+ }
260
265
  return { name, dartType: "String", toJson: "scalar" };
261
266
  }
262
267
  case "ZodNumber":
@@ -384,6 +389,12 @@ interface AggFieldSpec {
384
389
  * `undefined` for a non-map field.
385
390
  */
386
391
  mapValueKind?: FieldKind;
392
+ /**
393
+ * For a `t.map(VO.field())` field: the inner VO's generated Dart class name. The map
394
+ * surfaces as `Map<String, VoClass>` and decodes each entry via `VoClass.fromProjected`
395
+ * (per-entry typed decode). `undefined` for a map of scalars / plain json.
396
+ */
397
+ mapValueVoClass?: string;
387
398
  /**
388
399
  * For a `json`-kind field lowered from a NAMED VALUE OBJECT (`ValueObject.field()`):
389
400
  * the generated Dart class name. The field decodes via `readJsonObject…` +
@@ -697,13 +708,20 @@ function aggFieldSpec(
697
708
  // every other map keeps the flat-string `Map<String, String>`. The wire driver is
698
709
  // identical for both, so this is a frontend-only type choice (no hash change).
699
710
  const jsonValued = field.mapValueKind === "json";
711
+ const mapVoClass = field.mapValueVo?.name;
700
712
  return {
701
713
  name,
702
714
  wireName,
703
715
  kind: field.kind,
704
- dartType: jsonValued ? "Map<String, Object?>" : "Map<String, String>",
716
+ dartType:
717
+ mapVoClass !== undefined
718
+ ? `Map<String, ${mapVoClass}>`
719
+ : jsonValued
720
+ ? "Map<String, Object?>"
721
+ : "Map<String, String>",
705
722
  optional: false,
706
723
  ...(field.mapValueKind !== undefined ? { mapValueKind: field.mapValueKind } : {}),
724
+ ...(mapVoClass !== undefined ? { mapValueVoClass: mapVoClass } : {}),
707
725
  };
708
726
  }
709
727
  case "hasMany":
@@ -1237,6 +1255,15 @@ const geometryCoordsDart: Record<string, string> = {
1237
1255
  Point: "List<double>",
1238
1256
  LineString: "List<List<double>>",
1239
1257
  Polygon: "List<List<List<double>>>",
1258
+ MultiPoint: "List<List<double>>",
1259
+ };
1260
+
1261
+ /** The `geo.*` factory name for a geometry kind (used in generated doc comments). */
1262
+ const geometryFactoryNameDart: Record<string, string> = {
1263
+ Point: "point",
1264
+ LineString: "lineString",
1265
+ Polygon: "polygon",
1266
+ MultiPoint: "multiPoint",
1240
1267
  };
1241
1268
 
1242
1269
  /**
@@ -1296,6 +1323,7 @@ function voEnumFields(d: ValueObjectDescriptor): { fieldName: string; values: re
1296
1323
  const walk = (fieldName: string, f: ValueObjectFieldDescriptor, owner?: string): void => {
1297
1324
  if (f.type === "enum") out.push({ fieldName, values: f.values, ...(owner !== undefined ? { owner } : {}) });
1298
1325
  else if (f.type === "array") walk(fieldName, f.items, owner);
1326
+ else if (f.type === "map") walk(fieldName, f.values, owner);
1299
1327
  };
1300
1328
  for (const [fieldName, f] of Object.entries(d.fields ?? {})) walk(fieldName, f);
1301
1329
  // union arms: enums are owned by the ARM CLASS (`<Union><ArmCap>`) so two arms' same-named
@@ -1318,7 +1346,7 @@ function unionArmClassName(unionName: string, armKey: string): string {
1318
1346
  function dartTypeOfVoField(f: ValueObjectFieldDescriptor, voName: string, fieldName: string): string {
1319
1347
  switch (f.type) {
1320
1348
  case "string":
1321
- return "String";
1349
+ return f.brand ?? "String"; // a brandedId(...) → the generated extension-type wrapper
1322
1350
  case "number":
1323
1351
  // a UNIT-tagged number is the generated zero-cost extension type; `.int()` is Dart int.
1324
1352
  if (f.unit !== undefined) return f.unit;
@@ -1332,6 +1360,8 @@ function dartTypeOfVoField(f: ValueObjectFieldDescriptor, voName: string, fieldN
1332
1360
  return "String"; // surfaced as a constant getter, not a ctor param
1333
1361
  case "array":
1334
1362
  return `List<${dartTypeOfVoField(f.items, voName, fieldName)}>`;
1363
+ case "map":
1364
+ return `Map<String, ${dartTypeOfVoField(f.values, voName, fieldName)}>`;
1335
1365
  case "object":
1336
1366
  case "geometry":
1337
1367
  return f.ref; // the named VO class (emitted alongside / imported)
@@ -1349,7 +1379,7 @@ function voFromExpr(
1349
1379
  ): string {
1350
1380
  switch (f.type) {
1351
1381
  case "string":
1352
- return `${expr} as String`;
1382
+ return f.brand !== undefined ? `${f.brand}(${expr} as String)` : `${expr} as String`;
1353
1383
  case "enum":
1354
1384
  // TOLERANT typed decode — an unknown wire value becomes the sentinel case.
1355
1385
  return `${voEnumName(voName, fieldName)}.fromWire(${expr} as String)`;
@@ -1360,6 +1390,10 @@ function voFromExpr(
1360
1390
  return `${expr} as String`;
1361
1391
  case "boolean":
1362
1392
  return `${expr} as bool`;
1393
+ case "map": {
1394
+ const kv = `k${depth}`, vv = `v${depth}`;
1395
+ return `(${expr} as Map).map((${kv}, ${vv}) => MapEntry(${kv} as String, ${voFromExpr(f.values, vv, voName, fieldName, depth + 1)}))`;
1396
+ }
1363
1397
  case "array": {
1364
1398
  const v = `e${depth}`;
1365
1399
  return `(${expr} as List).map((${v}) => ${voFromExpr(f.items, v, voName, fieldName, depth + 1)}).toList()`;
@@ -1376,11 +1410,16 @@ function voToJsonExpr(f: ValueObjectFieldDescriptor, expr: string, depth = 0): s
1376
1410
  case "number":
1377
1411
  return f.unit !== undefined ? `${expr}.value` : expr;
1378
1412
  case "string":
1413
+ return f.brand !== undefined ? `${expr}.value` : expr; // a brandedId extension type → its wire string
1379
1414
  case "boolean":
1380
1415
  case "literal":
1381
1416
  return expr;
1382
1417
  case "enum":
1383
1418
  return `${expr}.wire`; // the exact wire string the law's zod enum validates
1419
+ case "map": {
1420
+ const inner = voToJsonExpr(f.values, `v${depth}`, depth + 1);
1421
+ return inner === `v${depth}` ? expr : `${expr}.map((k${depth}, v${depth}) => MapEntry(k${depth}, ${inner}))`;
1422
+ }
1384
1423
  case "array": {
1385
1424
  const inner = voToJsonExpr(f.items, `e${depth}`, depth + 1);
1386
1425
  return inner === `e${depth}` ? expr : `${expr}.map((e${depth}) => ${inner}).toList()`;
@@ -1656,6 +1695,7 @@ function emitValueObjectClass(d: ValueObjectDescriptor): string {
1656
1695
  Point: `(m['coordinates']! as List).map((e0) => (e0 as num).toDouble()).toList()`,
1657
1696
  LineString: `(m['coordinates']! as List).map((e0) => (e0 as List).map((e1) => (e1 as num).toDouble()).toList()).toList()`,
1658
1697
  Polygon: `(m['coordinates']! as List).map((e0) => (e0 as List).map((e1) => (e1 as List).map((e2) => (e2 as num).toDouble()).toList()).toList()).toList()`,
1698
+ MultiPoint: `(m['coordinates']! as List).map((e0) => (e0 as List).map((e1) => (e1 as num).toDouble()).toList()).toList()`,
1659
1699
  };
1660
1700
  return [
1661
1701
  `/// RFC 7946 ${d.geometry} — a \`geo.*\` value object ('${d.name}'). Positions are`,
@@ -1767,9 +1807,11 @@ function collectionDefaultParam(s: AggFieldSpec): string {
1767
1807
  case "set":
1768
1808
  return `this.${s.name} = const <String>{},`;
1769
1809
  case "map":
1770
- return isJsonValuedMap(s)
1771
- ? `this.${s.name} = const <String, Object?>{},`
1772
- : `this.${s.name} = const <String, String>{},`;
1810
+ return s.mapValueVoClass !== undefined
1811
+ ? `this.${s.name} = const <String, ${s.mapValueVoClass}>{},`
1812
+ : isJsonValuedMap(s)
1813
+ ? `this.${s.name} = const <String, Object?>{},`
1814
+ : `this.${s.name} = const <String, String>{},`;
1773
1815
  default:
1774
1816
  // An optional scalar — nullable, no default needed (defaults to null).
1775
1817
  return `this.${s.name},`;
@@ -1860,6 +1902,10 @@ function parseField(s: AggFieldSpec, aggregate: string): string {
1860
1902
  // A `json`-valued map decodes each entry value back to its OBJECT (so a
1861
1903
  // value-object map — e.g. `customFields`, each entry the canonical
1862
1904
  // `CustomField.toJson()` — round-trips losslessly); a string-valued map stays flat.
1905
+ if (s.mapValueVoClass !== undefined) {
1906
+ // A map of a NAMED value object: decode each entry to its typed VO class.
1907
+ return `readJsonMapOrEmpty(data, '${aggregate}', '${s.wireName}').map((k, v) => MapEntry(k, ${s.mapValueVoClass}.fromProjected((v! as Map).cast<String, Object?>())))`;
1908
+ }
1863
1909
  return isJsonValuedMap(s)
1864
1910
  ? `readJsonMapOrEmpty(data, '${aggregate}', '${s.wireName}')`
1865
1911
  : `readStrMapOrEmpty(data, '${aggregate}', '${s.wireName}')`;
@@ -1917,7 +1963,11 @@ function readModelDartType(s: AggFieldSpec): string {
1917
1963
  case "set":
1918
1964
  return "Set<String>";
1919
1965
  case "map":
1920
- return isJsonValuedMap(s) ? "Map<String, Object?>" : "Map<String, String>";
1966
+ return s.mapValueVoClass !== undefined
1967
+ ? `Map<String, ${s.mapValueVoClass}>`
1968
+ : isJsonValuedMap(s)
1969
+ ? "Map<String, Object?>"
1970
+ : "Map<String, String>";
1921
1971
  }
1922
1972
  }
1923
1973
 
@@ -1929,9 +1979,11 @@ function readModelDefaultParam(s: AggFieldSpec): string {
1929
1979
  case "set":
1930
1980
  return `this.${s.name} = const <String>{},`;
1931
1981
  case "map":
1932
- return isJsonValuedMap(s)
1933
- ? `this.${s.name} = const <String, Object?>{},`
1934
- : `this.${s.name} = const <String, String>{},`;
1982
+ return s.mapValueVoClass !== undefined
1983
+ ? `this.${s.name} = const <String, ${s.mapValueVoClass}>{},`
1984
+ : isJsonValuedMap(s)
1985
+ ? `this.${s.name} = const <String, Object?>{},`
1986
+ : `this.${s.name} = const <String, String>{},`;
1935
1987
  default:
1936
1988
  // An optional scalar, an optional OBJECT json leaf (nullable `Map`), OR a
1937
1989
  // permissive plain-json leaf (already-nullable `Object?`) — nullable, defaults to
@@ -2006,6 +2058,10 @@ function parseReadModelField(s: AggFieldSpec, aggregate: string): string {
2006
2058
  case "set":
2007
2059
  return `readSetOrEmpty(data, '${aggregate}', '${s.wireName}')`;
2008
2060
  case "map":
2061
+ if (s.mapValueVoClass !== undefined) {
2062
+ // A map of a NAMED value object: decode each entry to its typed VO class.
2063
+ return `readJsonMapOrEmpty(data, '${aggregate}', '${s.wireName}').map((k, v) => MapEntry(k, ${s.mapValueVoClass}.fromProjected((v! as Map).cast<String, Object?>())))`;
2064
+ }
2009
2065
  return isJsonValuedMap(s)
2010
2066
  ? `readJsonMapOrEmpty(data, '${aggregate}', '${s.wireName}')`
2011
2067
  : `readStrMapOrEmpty(data, '${aggregate}', '${s.wireName}')`;
@@ -3267,9 +3323,14 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
3267
3323
  // in local VO descriptors OR directive payloads gets ONE zero-cost wrapper. Opaque by design —
3268
3324
  // no generated arithmetic/conversions (a physics library is not a type system).
3269
3325
  const unitNames = new Set<string>();
3326
+ // brandedId(...) names → their own zero-cost String extension type (the standalone twin of
3327
+ // the aggregate typed-id classes). Collected from VO descriptors AND directive payloads.
3328
+ const brandNames = new Set<string>();
3270
3329
  const walkUnitField = (f: ValueObjectFieldDescriptor): void => {
3271
3330
  if (f.type === "number" && f.unit !== undefined) unitNames.add(f.unit);
3331
+ else if (f.type === "string" && f.brand !== undefined) brandNames.add(f.brand);
3272
3332
  else if (f.type === "array") walkUnitField(f.items);
3333
+ else if (f.type === "map") walkUnitField(f.values);
3273
3334
  };
3274
3335
  for (const d of localValueObjects) {
3275
3336
  for (const f of Object.values(d.fields ?? {})) walkUnitField(f);
@@ -3282,7 +3343,22 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
3282
3343
  if (schema && typeof schema === "object" && zodTypeName(schema as z.ZodTypeAny) === "ZodObject") {
3283
3344
  try {
3284
3345
  for (const spec of introspectPayload(schema as z.ZodTypeAny, new Map())) {
3285
- if (spec.toJson === "wrapped" && !spec.dartType.endsWith("Id")) unitNames.add(spec.dartType);
3346
+ // A spec whose brand a VO descriptor already registered is a brandedId, not a unit —
3347
+ // and neither is an aggregate typed-id (`<Agg>Id`, emitted by the id machinery).
3348
+ if (spec.toJson === "wrapped" && !spec.dartType.endsWith("Id") && !brandNames.has(spec.dartType)) {
3349
+ unitNames.add(spec.dartType);
3350
+ }
3351
+ }
3352
+ // Directly-scanned brandedId payload fields (incl. `<Brand>` ending in "Id", which the
3353
+ // unit filter skips) — the payload-only lane still emits their extension type.
3354
+ const rawShape = (zodDef(schema as z.ZodTypeAny) as { shape?: Record<string, z.ZodTypeAny> | (() => Record<string, z.ZodTypeAny>) }).shape;
3355
+ const shp = typeof rawShape === "function" ? rawShape() : rawShape ?? {};
3356
+ for (let zt of Object.values(shp)) {
3357
+ for (let hops = 0; hops < 8 && (zodTypeName(zt) === "ZodOptional" || zodTypeName(zt) === "ZodDefault" || zodTypeName(zt) === "ZodArray"); hops++) {
3358
+ zt = zodTypeName(zt) === "ZodArray" ? zodArrayElement(zt) : zodInnerType(zt);
3359
+ }
3360
+ const b = brandedIdMetaOf(zt);
3361
+ if (b !== undefined) brandNames.add(b);
3286
3362
  }
3287
3363
  } catch { /* an unsupported payload shape is reported by the payload emitter, not here */ }
3288
3364
  }
@@ -3293,8 +3369,14 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
3293
3369
  `/// error. Opaque tag only — no arithmetic/conversions are generated.`,
3294
3370
  `extension type const ${u}(double value) {}`,
3295
3371
  ].join("\n")).join("\n\n");
3372
+ const brandBlock = [...brandNames].sort().map((b) => [
3373
+ `/// BRANDED ID \`${b}\` — a ZERO-COST nominal String (Dart extension type; \`brandedId("${b}")\`):`,
3374
+ `/// passing a bare String (or another brand) where [${b}] is required is a COMPILE error.`,
3375
+ `extension type const ${b}(String value) {}`,
3376
+ ].join("\n")).join("\n\n");
3296
3377
  const voBlock = [
3297
3378
  ...(unitBlock !== "" ? [unitBlock] : []),
3379
+ ...(brandBlock !== "" ? [brandBlock] : []),
3298
3380
  ...(localValueObjects.length > 0
3299
3381
  ? [
3300
3382
  emitVoHelpers(),
@@ -360,7 +360,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
360
360
  `// client file gives an app the whole typed surface.`,
361
361
  `export '${mod.name}.dart';`,
362
362
  ...(opts.frameworkImportUri !== undefined ? [`export '${opts.frameworkImportUri}';`] : []),
363
- `export '${supportImport}' show NomosBridge, BridgeTransport, NomosBridgeError, MissingOfferAuthority, kWebviewBridgeVersion${encPresent ? ", NomosScopeKey, NomosKeyStore, EncryptionKeyUnavailable, WrappedKeyStore, NomosEncryption, NomosEpochResult, EncryptionMemberKeyMissing, EncryptionRosterMismatch" : ""};`,
363
+ `export '${supportImport}' show NomosBridge, BridgeTransport, NomosBridgeError, MissingOfferAuthority, kWebviewBridgeVersion, NomosCompat, NomosCompatMember${encPresent ? ", NomosScopeKey, NomosKeyStore, EncryptionKeyUnavailable, WrappedKeyStore, NomosEncryption, NomosEpochResult, EncryptionMemberKeyMissing, EncryptionRosterMismatch" : ""};`,
364
364
  ``,
365
365
  `/// sha256 of the deployed \`.package.usda\` — the installed law this client offers under.`,
366
366
  `const String ${hashConst} = ${dartStr(opts.domainHash)};`,
@@ -440,6 +440,23 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
440
440
  ` /// the workspace's installed law changes (a synced install/upgrade from another peer).`,
441
441
  ` Stream<Map<String, Object?>> watchCompatibility() => bridge.watchCompatibility(_requirement);`,
442
442
  ``,
443
+ ...(opts.compat !== undefined
444
+ ? [
445
+ ` /// THE COMPAT SUPERPOWER (#72b): compute the LIVE per-member verdict for THIS client against the`,
446
+ ` /// workspace's on-ledger offer + arm the pre-call refusal. Await it after connect so a KNOWN-`,
447
+ ` /// incompatible directive throws [LawMismatchError] CLIENT-SIDE at write time (never a server 422).`,
448
+ ` Future<NomosCompat?> checkCompat() => bridge.checkCompat(`,
449
+ ` (jsonDecode(${compatConstNameDart(mod.name)}) as Map).cast<String, Object?>(), _requirement);`,
450
+ ``,
451
+ ` /// THE FEATURE FLAG (dark-launch): true iff [member] exists on the offer AND is compatible — ship`,
452
+ ` /// UI with a member dark, deploy the law, it lights up. Await [checkCompat] once first to arm it.`,
453
+ ` Future<bool> has(String member) => bridge.compatHas(member);`,
454
+ ``,
455
+ ` /// COMPAT SUPERPOWER live events: emits the fresh verdict when a synced law change moves the offer.`,
456
+ ` Stream<NomosCompat> get onLawChanged => bridge.onLawChanged;`,
457
+ ``,
458
+ ]
459
+ : []),
443
460
  ]
444
461
  : []),
445
462
  ...methods,
@@ -170,7 +170,7 @@ export function generateOpenApiFragment(
170
170
  }
171
171
  for (const sp of module.spatials ?? []) {
172
172
  getRead(sp.id, `spatial: ${sp.of} by ${sp.on} bbox`, { kind: "spatial", spatialId: sp.id, on: sp.on }, [
173
- { name: "bbox", in: "query", required: true, schema: { type: "string" }, description: "minLng,minLat,maxLng,maxLat" },
173
+ { name: "bbox", in: "query", required: true, schema: { type: "string" }, description: "minX,minY,maxX,maxY" },
174
174
  ]);
175
175
  }
176
176
 
@@ -97,19 +97,68 @@ function sampleString(zt: z.ZodTypeAny, field: string): string {
97
97
  return readable;
98
98
  }
99
99
 
100
- /** One sample value for one zod shape. NO FALLBACK: an unhandled shape throws, naming its remedy. */
101
- function sampleOfZod(zt: z.ZodTypeAny, field: string, where: string, voIncludeOptional = false): 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.
100
+ // Candidate numbers probed against a field's own schema (in a sensible order) so a `.min()/.max()/
101
+ // .int()/.positive()` constraint is respected — the number sibling of SUBJECT_CANDIDATES. `1` first
102
+ // keeps the guestbook golden output stable; the rest cover common bounds (lat/lng, non-negative, etc.).
103
+ const NUMBER_CANDIDATES: readonly number[] = [1, 0, -1, 2, 10, 100, 0.5, -90, 90, 180, -180, 1000000];
104
+ function sampleNumber(zt: z.ZodTypeAny): number {
105
+ for (const cand of NUMBER_CANDIDATES) if (zt.safeParse(cand).success) return cand;
106
+ return 1; // no candidate fits an exotic bound — the whole-payload safeParse / VO validate names the field
107
+ }
108
+
109
+ /** The leaf identifier of a full path (`a.b[0].c` → `c`) — the readable `sample-<leaf>` string base. */
110
+ function leafOfPath(path: string): string {
111
+ const m = path.match(/([A-Za-z0-9_$]+)(?:\[\d+\])*$/);
112
+ return m ? m[1]! : path;
113
+ }
114
+
115
+ /** Append zod-issue path segments (numeric → `[i]`, string → `.name`) onto a base path. */
116
+ function joinIssuePath(base: string, segs: readonly PropertyKey[]): string {
117
+ let out = base;
118
+ for (const s of segs) out += typeof s === "number" ? `[${s}]` : out.length ? `.${String(s)}` : String(s);
119
+ return out;
120
+ }
121
+
122
+ /**
123
+ * One sample value for one zod shape, threading the FULL PATH from the payload root so any failure
124
+ * names the exact field (`controlPoints[0].geoPoint.latitude`, never a bare leaf or "field ?"). NO
125
+ * FALLBACK: an unhandled shape throws, naming the path and the remedy.
126
+ */
127
+ function sampleOfZod(zt: z.ZodTypeAny, path: string, where: string, voIncludeOptional = false): unknown {
128
+ // VALUE OBJECTS FIRST (vo_authoring_end_to_end.md slice C): a declared VO carries its own lawful
129
+ // sample (the author's validated `example`, the blessed geometry sample, or per-field descriptor
130
+ // synthesis, recursing through nested VOs / arrays-of-VOs / unions). The raw-zod walk below cannot
131
+ // see interior constraints (geo's min-2 coordinates, a `.min()` on a subfield), so after building the
132
+ // descriptor-shaped sample we RE-VALIDATE it against the VO's OWN zod: a violation the descriptor
133
+ // couldn't capture is reported at its EXACT interior path with the escape hatch (declare an example).
106
134
  const vo = voMetaOf(zt);
107
- if (vo !== undefined) return voSample(vo, { includeOptional: voIncludeOptional });
135
+ if (vo !== undefined) {
136
+ const sample = voSample(vo, { includeOptional: voIncludeOptional, path });
137
+ // Re-validate against a REAL zod: `zt` is either a genuine ZodType (has safeParse) or the VO HANDLE
138
+ // (a first-class schema whose validator lives on `.zod`). Skip only if neither is callable.
139
+ const validator: z.ZodTypeAny | undefined =
140
+ typeof (zt as { safeParse?: unknown }).safeParse === "function"
141
+ ? zt
142
+ : typeof ((zt as { zod?: { safeParse?: unknown } }).zod?.safeParse) === "function"
143
+ ? (zt as unknown as { zod: z.ZodTypeAny }).zod
144
+ : undefined;
145
+ if (vo.example === undefined && validator !== undefined) {
146
+ const parsed = validator.safeParse(sample);
147
+ if (!parsed.success) {
148
+ const iss = parsed.error.issues[0];
149
+ const at = joinIssuePath(path, iss?.path ?? []);
150
+ throw new Error(
151
+ `codegen-proof: ${where} — field '${at}': the synthesized sample violates value object '${vo.name}''s schema (${iss?.message ?? "invalid"}) — the descriptor cannot see this constraint, so declare a lawful \`example\` on valueObject("${vo.name}", …, { example: … }) and proof synthesis will use it instead of guessing the interior`,
152
+ );
153
+ }
154
+ }
155
+ return sample;
156
+ }
108
157
  switch (zodKind(zt)) {
109
158
  case "string":
110
- return sampleString(zt, field);
159
+ return sampleString(zt, leafOfPath(path));
111
160
  case "number":
112
- return 1;
161
+ return sampleNumber(zt);
113
162
  case "boolean":
114
163
  return true;
115
164
  case "enum":
@@ -120,14 +169,17 @@ function sampleOfZod(zt: z.ZodTypeAny, field: string, where: string, voIncludeOp
120
169
  }
121
170
  case "array": {
122
171
  const el = zodArrayElement(zt);
123
- return zodKind(el) === "string" ? ["sample-a"] : [sampleOfZod(el, field, where, voIncludeOptional)];
172
+ // A one-element array satisfies `.min(1)` coverage; a string element keeps the readable
173
+ // `sample-a` (constraint-probed) so golden outputs stay stable.
174
+ if (zodKind(el) === "string") return [sampleString(el, "a")];
175
+ return [sampleOfZod(el, `${path}[0]`, where, voIncludeOptional)];
124
176
  }
125
177
  case "object": {
126
178
  const out: Record<string, unknown> = {};
127
179
  for (const [name, raw] of Object.entries(zodObjectShape(zt))) {
128
180
  const k = zodKind(raw as z.ZodTypeAny);
129
181
  if (k === "optional" || k === "default") continue; // optional → omit
130
- out[name] = sampleOfZod(raw as z.ZodTypeAny, name, where, voIncludeOptional);
182
+ out[name] = sampleOfZod(raw as z.ZodTypeAny, joinIssuePath(path, [name]), where, voIncludeOptional);
131
183
  }
132
184
  return out;
133
185
  }
@@ -136,15 +188,15 @@ function sampleOfZod(zt: z.ZodTypeAny, field: string, where: string, voIncludeOp
136
188
  case "nullable":
137
189
  case "optional":
138
190
  case "default":
139
- return sampleOfZod(zodDef(zt).innerType!, field, where, voIncludeOptional);
191
+ return sampleOfZod(zodDef(zt).innerType!, path, where, voIncludeOptional);
140
192
  case "union": {
141
193
  const first = zodDef(zt).options?.[0];
142
- if (first !== undefined) return sampleOfZod(first, field, where, voIncludeOptional);
143
- throw new Error(`codegen-proof: empty union at field '${field}' of ${where}`);
194
+ if (first !== undefined) return sampleOfZod(first, path, where, voIncludeOptional);
195
+ throw new Error(`codegen-proof: ${where} — field '${path}': an empty union has no arm to sample`);
144
196
  }
145
197
  default:
146
198
  throw new Error(
147
- `codegen-proof: cannot synthesize a sample for zod shape '${zodKind(zt)}' (field '${field}' of ${where}) — reshape the payload to a sampleable type, or prove that directive by hand in test/e2e.mts`,
199
+ `codegen-proof: ${where} — field '${path}': cannot synthesize a sample for zod shape '${zodKind(zt)}' — reshape it to a sampleable type (string/number/boolean/enum/array/object/nested valueObject), or prove that directive by hand in test/e2e.mts`,
148
200
  );
149
201
  }
150
202
  }
@@ -166,7 +218,7 @@ export function synthesizePayload(schema: z.ZodTypeAny, where: string, opts?: {
166
218
  const k = zodKind(f);
167
219
  if (k === "optional" || k === "default") continue; // optional → omit
168
220
  if (stamped.has(name)) continue; // `…At` → omit; the client stamps it
169
- value[name] = sampleOfZod(f, name, where, opts?.voIncludeOptional === true);
221
+ value[name] = sampleOfZod(f, name, where, opts?.voIncludeOptional === true); // path root = the payload field name
170
222
  }
171
223
  return { value, autoStamped };
172
224
  }
@@ -1735,6 +1787,12 @@ export interface ProofLegs {
1735
1787
  * discoverable-but-suspect (the leg's `recipeIssues`). Printed by the compile summary; the offline
1736
1788
  * runner fails fast on recipe issues with the same words. Additive: absent on older files. */
1737
1789
  readonly birthDiagnostics?: readonly string[];
1790
+ /** PROOF TAXONOMY DIAGNOSTICS (co2 ask: every directive is either PROVEN or NAMED-WHY): one line per
1791
+ * creating directive that drops out of the offline proof, saying exactly why — a payload the synthesizer
1792
+ * couldn't sample (carrying the EXACT field path from {@link synthesizePayload}), an attestation-authorized
1793
+ * create (needs a signed §3 quote), or a gated create with no seedable authority (the relation-bootstrap
1794
+ * gap). Never a silent drop. Printed by the compile summary. Additive: absent on older files. */
1795
+ readonly proofDiagnostics?: readonly string[];
1738
1796
  /** SPATIAL MEMBERSHIP (lane-2 read-serving): present when a declared spatial index covers the created
1739
1797
  * aggregate AND the plan-probe proved the synthesized payload lands a geometry on the indexed path —
1740
1798
  * the offline runner asserts `spatialWithin(id, world)` contains the created row. */
@@ -1927,10 +1985,49 @@ export function legsForDomain(mod: DomainModule, modules: readonly DomainModule[
1927
1985
  for (const issue of r.leg?.recipeIssues ?? []) birthDiagnostics.push(`${domain}/${d.id}: ${issue}`);
1928
1986
  }
1929
1987
 
1988
+ // PROOF TAXONOMY AUDIT (co2 ask: every creating directive is PROVEN or NAMED-WHY). Walk every `.creates`
1989
+ // directive; if it is NOT represented by a proven leg, say exactly why — the synthesis failure names the
1990
+ // EXACT unsampleable field path; an attestation/gated create names the lane that DOES prove it.
1991
+ const proofDiagnostics: string[] = [];
1992
+ const provenCreateIds = new Set<string>();
1993
+ if (standalone !== null) {
1994
+ if (standalone.directiveId !== undefined) provenCreateIds.add(standalone.directiveId);
1995
+ for (const ec of standalone.extraCreates ?? []) provenCreateIds.add(ec.directiveId);
1996
+ }
1997
+ if (relationBootstrap !== undefined) provenCreateIds.add(relationBootstrap.gatedDirectiveId);
1998
+ for (const d of mod.directives as Directive<unknown>[]) {
1999
+ if (d?.marker !== "creates" || typeof d.payloadSchema !== "object" || d.birthsChild === true) continue;
2000
+ if (provenCreateIds.has(d.id)) continue; // proven by some leg — nothing to name
2001
+ const who = `${domain}/${d.id}`;
2002
+ if (d.attestationAuthorizedGate !== undefined) {
2003
+ proofDiagnostics.push(
2004
+ `${who}: creates ${d.aggregateId} but is ATTESTATION-AUTHORIZED (needs a signed §3 attestation quote no synthesizer can produce) — proven via the born-child / attestation lane, not the standalone offline proof`,
2005
+ );
2006
+ continue;
2007
+ }
2008
+ if (d.requiresRelationGate !== undefined) {
2009
+ // A gated create with no discoverable authority path (else it would be in provenCreateIds via the
2010
+ // relation-bootstrap leg) — name the exact seeding gap (the negative conformance leg still proves the gate).
2011
+ const gap = diagnoseRelationBootstrapGap(mod);
2012
+ proofDiagnostics.push(gap ?? `${who}: creates ${d.aggregateId} gated by .requires("${d.requiresRelationGate.relation}") — no seedable authority path found; prove it by hand or expose a granter/seed`);
2013
+ continue;
2014
+ }
2015
+ // An UNGATED standalone create that still isn't proven ⇒ its payload could not be synthesized. Re-run the
2016
+ // synthesis to capture the exact-path reason (createLegFor swallowed it to keep the index resilient).
2017
+ try {
2018
+ synthesizePayload(d.payloadSchema as z.ZodTypeAny, who);
2019
+ // Synthesis succeeded but the leg was still dropped (e.g. the aggregate isn't composed) — name that.
2020
+ proofDiagnostics.push(`${who}: creates ${d.aggregateId} but no offline leg was built (its aggregate '${d.aggregateId}' is not composed in this package, or its create-leg probe failed) — prove it by hand in test/e2e.mts`);
2021
+ } catch (e) {
2022
+ proofDiagnostics.push(`${who}: creates ${d.aggregateId} but its payload could not be synthesized for the offline proof — ${(e as Error)?.message ?? e}`);
2023
+ }
2024
+ }
2025
+
1930
2026
  return {
1931
2027
  domain,
1932
2028
  legs,
1933
2029
  ...(birthDiagnostics.length > 0 ? { birthDiagnostics } : {}),
2030
+ ...(proofDiagnostics.length > 0 ? { proofDiagnostics } : {}),
1934
2031
  ...(standalone !== null
1935
2032
  ? {
1936
2033
  directiveId: standalone.directiveId,