@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/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 +55 -9
- package/src/index.ts +14 -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/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
|
-
/**
|
|
56
|
-
|
|
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
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
|
143
|
-
|
|
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
|
package/src/codegen_dart.ts
CHANGED
|
@@ -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:
|
|
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
|
|
1771
|
-
? `this.${s.name} = const <String,
|
|
1772
|
-
:
|
|
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
|
|
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
|
|
1933
|
-
? `this.${s.name} = const <String,
|
|
1934
|
-
:
|
|
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
|
-
|
|
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,
|
package/src/codegen_openapi.ts
CHANGED
|
@@ -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: "
|
|
173
|
+
{ name: "bbox", in: "query", required: true, schema: { type: "string" }, description: "minX,minY,maxX,maxY" },
|
|
174
174
|
]);
|
|
175
175
|
}
|
|
176
176
|
|
package/src/codegen_proof.ts
CHANGED
|
@@ -97,19 +97,68 @@ function sampleString(zt: z.ZodTypeAny, field: string): string {
|
|
|
97
97
|
return readable;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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)
|
|
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,
|
|
159
|
+
return sampleString(zt, leafOfPath(path));
|
|
111
160
|
case "number":
|
|
112
|
-
return
|
|
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
|
-
|
|
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!,
|
|
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,
|
|
143
|
-
throw new Error(`codegen-proof:
|
|
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)}'
|
|
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,
|