@githolon/dsl 0.32.0 → 0.34.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/aggregate.ts +12 -1
- package/src/authoring.ts +4 -1
- package/src/birth_meter.ts +300 -0
- package/src/birth_meter_ensure.ts +67 -0
- package/src/build_package.ts +59 -0
- package/src/codegen_dart.ts +347 -3
- package/src/codegen_ts.ts +74 -1
- package/src/compile_engine.ts +13 -2
- package/src/compile_package_main.ts +42 -2
- package/src/directive.ts +10 -1
- package/src/engine_entry.ts +56 -9
- package/src/fields.ts +84 -0
- package/src/framework/birth_meter.ts +125 -0
- package/src/framework/impure_capability.ts +5 -2
- package/src/framework/platform_admin.ts +4 -1
- package/src/framework/relations.ts +19 -0
- package/src/framework/workspaces.ts +42 -1
- package/src/index.ts +53 -0
- package/src/read.ts +22 -0
- package/src/read_projection_composition.ts +117 -0
- package/src/read_projection_manifest.ts +94 -0
- package/src/read_projection_namespace.ts +152 -0
- package/src/usd.ts +141 -10
- package/src/usd_layers.ts +82 -0
- package/src/value_object.ts +456 -0
- package/src/wire_encode.ts +34 -3
package/src/build_package.ts
CHANGED
|
@@ -54,8 +54,15 @@ import type { ImpureCapabilityDecl } from "./codegen_provider_dart.js";
|
|
|
54
54
|
import type { WorkspaceTypeDecl } from "./workspace_type.js";
|
|
55
55
|
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
56
56
|
import { synthesizePreconditionInvariants } from "./precondition.js";
|
|
57
|
+
import {
|
|
58
|
+
BIRTH_METER_AGGREGATES,
|
|
59
|
+
BIRTH_METER_QUERIES,
|
|
60
|
+
synthesizeBirthQuotaInvariants,
|
|
61
|
+
} from "./birth_meter.js";
|
|
62
|
+
import { withBirthMeterExports } from "./birth_meter_ensure.js";
|
|
57
63
|
import type { RelationTypeDecl } from "./relation_schema.js";
|
|
58
64
|
import { domainHash, emitManifestBytes } from "./manifest.js";
|
|
65
|
+
import { collectValueObjectDescriptors } from "./value_object.js";
|
|
59
66
|
import { emitUsd } from "./usd.js";
|
|
60
67
|
import { expandCapabilityExports } from "./capability_exports.js";
|
|
61
68
|
import { RelationTuple } from "./framework/relations.js";
|
|
@@ -145,6 +152,11 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
145
152
|
// `capability()` bundles expand into discoverable entries (the package plane's
|
|
146
153
|
// merge point) — the quartet validation below then passes without re-exports.
|
|
147
154
|
merged = expandCapabilityExports(merged);
|
|
155
|
+
// METERED BIRTH: inject the sanctioning `nomosBirthRecordEnsure` directive + meter aggregates
|
|
156
|
+
// into a `.births()`-declaring domain at the SAME seam the engine merge applies it — the
|
|
157
|
+
// Ensure→NomosBirthRecord manifest declaration is what admits the auto-appended record through
|
|
158
|
+
// the id-mint gate's declared-ensures lane. Birth-free domains are byte-identical (era rule).
|
|
159
|
+
merged = withBirthMeterExports(merged);
|
|
148
160
|
|
|
149
161
|
const byId = new Map<string, AggregateHandle>();
|
|
150
162
|
for (const a of [...aggregatesOf(merged), ...(spec.extraAggregates ?? [])]) byId.set(a.id, a);
|
|
@@ -157,6 +169,18 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
157
169
|
if (!byId.has(RelationTuple.id) && directives.some((d) => d.aggregateId === RelationTuple.id)) {
|
|
158
170
|
byId.set(RelationTuple.id, RelationTuple);
|
|
159
171
|
}
|
|
172
|
+
// BIRTH-METER AUTO-ENSURE (metered birth, slice A — the same "discoverable without re-exports"
|
|
173
|
+
// courtesy as RelationTuple): a domain declaring ≥1 `.births()` directive gets the meter aggregates
|
|
174
|
+
// (NomosBirthRecord/BirthPolicy/BirthGrant) injected so the auto-record folds + projects, and the
|
|
175
|
+
// meter queries injected so the captured reads route/re-derive. Era-safe: only a birth-declaring
|
|
176
|
+
// domain gains them, so no existing (birth-free) manifest hash drifts — and a birth-declaring domain
|
|
177
|
+
// recompiled under this DSL moves hash ANYWAY (the meter IS its new era).
|
|
178
|
+
const declaresBirth = directives.some((d) => (d as { birthsChild?: boolean }).birthsChild === true);
|
|
179
|
+
if (declaresBirth) {
|
|
180
|
+
for (const a of BIRTH_METER_AGGREGATES) {
|
|
181
|
+
if (!byId.has(a.id)) byId.set(a.id, a);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
160
184
|
const aggregates = [...byId.values()];
|
|
161
185
|
const directiveIds = new Set(directives.map((d) => d.id));
|
|
162
186
|
for (const task of spec.impureCapabilities ?? []) {
|
|
@@ -187,7 +211,39 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
187
211
|
// hand-authored `workspaceInvariant(...)` occupies — so downstream treats them identically. Era-safe:
|
|
188
212
|
// a directive with no precondition synthesizes nothing (byte-identical manifest).
|
|
189
213
|
for (const d of directives) workspaceInvariants.push(...synthesizePreconditionInvariants(d));
|
|
214
|
+
// METERED BIRTH (slice A2): synthesize THE BIRTH-QUOTA invariant for every `.births()` directive —
|
|
215
|
+
// the same desugaring lane as preconditions, so the manifest (`nomosInvariantGate`) and the sealed
|
|
216
|
+
// engine (which synthesizes from the SAME directives in `buildDomainSlice`) can never disagree.
|
|
217
|
+
for (const d of directives) workspaceInvariants.push(...synthesizeBirthQuotaInvariants(d));
|
|
218
|
+
// The meter's declared queries route like any exported query (read manifest + captured-read re-derive).
|
|
219
|
+
if (declaresBirth) {
|
|
220
|
+
const haveQuery = new Set(queries.map((q) => q.id));
|
|
221
|
+
for (const q of BIRTH_METER_QUERIES) {
|
|
222
|
+
if (!haveQuery.has(q.id)) queries.push(q);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
190
225
|
const relationTypes = [...(spec.relationTypes ?? [])];
|
|
226
|
+
// FIRST-CLASS VALUE OBJECTS (value_object.ts): the deduped, sorted descriptors of every
|
|
227
|
+
// named VO the domain's aggregates carry (transitive nested VOs included) — the input to
|
|
228
|
+
// the additive `nomosValueObjects` USD key. Omit-when-empty (era rule): a VO-free domain's
|
|
229
|
+
// module — and thus its USD/package bytes — is byte-identical to before VOs existed.
|
|
230
|
+
const valueObjects = collectValueObjectDescriptors(aggregates);
|
|
231
|
+
// SPATIAL × VO PAIRING (fail at build, not at fold): a spatial index over a VO-typed field
|
|
232
|
+
// must index a GEO value object (an RFC 7946 geometry — the R*Tree's bbox source). A
|
|
233
|
+
// structural VO (e.g. a PostalAddress) has no geometry to index. Non-VO fields (a
|
|
234
|
+
// hand-rolled `t.jsonObject()` GeoJSON leaf) pass through unchecked, exactly as before.
|
|
235
|
+
for (const s of spatials) {
|
|
236
|
+
const agg = byId.get(s.of);
|
|
237
|
+
const field = agg?.fields[s.on];
|
|
238
|
+
const vo = field?.valueObject;
|
|
239
|
+
if (vo !== undefined && vo.geometry === undefined) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`build-package: spatial '${s.id}' indexes '${s.of}.${s.on}', a '${vo.name}' value object with NO ` +
|
|
242
|
+
`geometry — the R*Tree needs an RFC 7946 geometry leaf. Give the geometry its OWN field ` +
|
|
243
|
+
`(e.g. \`point: geo.point().field()\`) and index that (v1: no sub-path indexing).`,
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
191
247
|
const dartImports = [...(spec.dartImports ?? [])];
|
|
192
248
|
// THE PEER-SOVEREIGN ROOT-OF-TRUST: a home/child law that exports `requireParentAttestedBirth` declares
|
|
193
249
|
// it requires a parent-attested-birth cert chain. Discovered by shape across the module list (the SAME
|
|
@@ -233,6 +289,9 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
233
289
|
// Omit-when-empty — the relation schema is hashed law (P1); a relation-free domain's
|
|
234
290
|
// canonical manifest/USD stays byte-identical (the nomosRelations era rule).
|
|
235
291
|
...(relationTypes.length > 0 ? { relationTypes } : {}),
|
|
292
|
+
// Omit-when-empty — the `nomosValueObjects` era rule: a VO-free domain's USD/package
|
|
293
|
+
// bytes are byte-identical to before value objects existed.
|
|
294
|
+
...(valueObjects.length > 0 ? { valueObjects } : {}),
|
|
236
295
|
// THE PEER-SOVEREIGN ROOT-OF-TRUST: omit-when-false — a law that doesn't require parent-attested
|
|
237
296
|
// birth is byte-identical in the canonical manifest/USD (the nomosParentAttestedBirth era rule).
|
|
238
297
|
...(parentAttestedBirth ? { parentAttestedBirth } : {}),
|
package/src/codegen_dart.ts
CHANGED
|
@@ -32,7 +32,7 @@ import { z } from "zod";
|
|
|
32
32
|
import type { AggregateHandle } from "./aggregate.js";
|
|
33
33
|
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
34
34
|
import type { RelationTypeDecl } from "./relation_schema.js";
|
|
35
|
-
import type { Field, FieldKind } from "./fields.js";
|
|
35
|
+
import type { Field, FieldKind, ValueObjectDescriptor, ValueObjectFieldDescriptor } from "./fields.js";
|
|
36
36
|
import type { Directive } from "./directive.js";
|
|
37
37
|
import { executeDirectiveToIntent } from "./wire_encode.js";
|
|
38
38
|
import { deterministicPorts } from "./ctx.js";
|
|
@@ -347,6 +347,13 @@ interface AggFieldSpec {
|
|
|
347
347
|
* `undefined` for a non-map field.
|
|
348
348
|
*/
|
|
349
349
|
mapValueKind?: FieldKind;
|
|
350
|
+
/**
|
|
351
|
+
* For a `json`-kind field lowered from a NAMED VALUE OBJECT (`ValueObject.field()`):
|
|
352
|
+
* the generated Dart class name. The field decodes via `readJsonObject…` +
|
|
353
|
+
* `<VoClass>.fromProjected(m)` (the EvidenceRef pattern) and types as the class.
|
|
354
|
+
* `undefined` for a plain `t.json()`/`t.jsonObject()` field.
|
|
355
|
+
*/
|
|
356
|
+
voClass?: string;
|
|
350
357
|
/**
|
|
351
358
|
* For a `json`-kind field: the asserted decoded SHAPE (`field.jsonShape`). `"object"`
|
|
352
359
|
* (a `t.jsonObject()` leaf, e.g. `location`) means the value is ALWAYS a JSON object —
|
|
@@ -581,8 +588,21 @@ function aggFieldSpec(
|
|
|
581
588
|
...(valueClass !== undefined ? { valueClass } : {}),
|
|
582
589
|
};
|
|
583
590
|
case "json":
|
|
591
|
+
// A NAMED VALUE OBJECT (`ValueObject.field()`) types as its generated Dart class
|
|
592
|
+
// (typed decode via `.fromProjected`, the EvidenceRef pattern). Otherwise
|
|
584
593
|
// `jsonShape` ("object" via `t.jsonObject()`) chooses strict-object vs
|
|
585
594
|
// permissive read-model type.
|
|
595
|
+
if (field.valueObject !== undefined) {
|
|
596
|
+
return {
|
|
597
|
+
name,
|
|
598
|
+
wireName,
|
|
599
|
+
kind: field.kind,
|
|
600
|
+
dartType: field.valueObject.name,
|
|
601
|
+
optional,
|
|
602
|
+
voClass: field.valueObject.name,
|
|
603
|
+
...(field.jsonShape !== undefined ? { jsonShape: field.jsonShape } : {}),
|
|
604
|
+
};
|
|
605
|
+
}
|
|
586
606
|
return {
|
|
587
607
|
name,
|
|
588
608
|
wireName,
|
|
@@ -1166,6 +1186,275 @@ function emitEvidenceRefClass(): string {
|
|
|
1166
1186
|
].join("\n");
|
|
1167
1187
|
}
|
|
1168
1188
|
|
|
1189
|
+
// ── value objects → Dart classes (`value_object.ts` descriptors) ─────────────────────
|
|
1190
|
+
|
|
1191
|
+
/** The Dart coordinates type per RFC 7946 geometry kind. */
|
|
1192
|
+
const geometryCoordsDart: Record<string, string> = {
|
|
1193
|
+
Point: "List<double>",
|
|
1194
|
+
LineString: "List<List<double>>",
|
|
1195
|
+
Polygon: "List<List<List<double>>>",
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* The generated Dart enum NAME for one VO enum field: `<VoName><FieldName>Enum`
|
|
1200
|
+
* (e.g. `PostalAddressCountryCodeEnum`). The VO is the field's semantic home — the
|
|
1201
|
+
* exact analogue of the aggregate-enum convention's domain prefix (`collectEnums`'s
|
|
1202
|
+
* `<Domain><Field>Enum`), so two barrel-exported domains never collide on a bare name.
|
|
1203
|
+
* IDENTITY = THE VO FIELD, not the value set: two identical value-sets in DIFFERENT
|
|
1204
|
+
* VOs stay separate types (VO names are package-unique, so so are these).
|
|
1205
|
+
*/
|
|
1206
|
+
function voEnumName(voName: string, fieldName: string): string {
|
|
1207
|
+
return `${voName}${dartTypeNameFromField(fieldName)}Enum`;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/** The tolerant-decode SENTINEL case name — dodge a literal enum value that would
|
|
1211
|
+
* collide with it (`dartIdent("unknownWire") === "unknownWire"`). */
|
|
1212
|
+
function voEnumSentinel(values: readonly string[]): string {
|
|
1213
|
+
let s = "unknownWire";
|
|
1214
|
+
while (values.some((v) => dartIdent(v) === s)) s = `${s}_`;
|
|
1215
|
+
return s;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
/** Emit ONE generated Dart enum for a VO enum field. TOLERANT DECODE (law evolves —
|
|
1219
|
+
* a new value added by a later law must NOT crash an old client): `fromWire` maps an
|
|
1220
|
+
* unrecognized wire string to the [sentinel] case instead of throwing (the deliberate
|
|
1221
|
+
* divergence from the strict aggregate-enum `fromWire`; VO data crosses law versions). */
|
|
1222
|
+
function emitVoEnum(voName: string, fieldName: string, values: readonly string[]): string {
|
|
1223
|
+
const name = voEnumName(voName, fieldName);
|
|
1224
|
+
const sentinel = voEnumSentinel(values);
|
|
1225
|
+
const variants = values.map((v) => ` ${dartIdent(v)}('${v}')`).join(",\n");
|
|
1226
|
+
return [
|
|
1227
|
+
`/// Generated enum for value object field '${voName}.${fieldName}': ${values.join(", ")}.`,
|
|
1228
|
+
`/// TOLERANT DECODE: an unrecognized wire value (law can add enum values; old`,
|
|
1229
|
+
`/// clients must not crash on new data) decodes to [${name}.${sentinel}] — never a throw.`,
|
|
1230
|
+
`/// [wire] is the exact wire string ([${sentinel}].wire is the '__unknown__' marker;`,
|
|
1231
|
+
`/// authoring code constructs KNOWN cases, so toJson never writes it in practice).`,
|
|
1232
|
+
`enum ${name} {`,
|
|
1233
|
+
`${variants},`,
|
|
1234
|
+
` /// The tolerant-decode sentinel — an enum value this client's law snapshot does not know.`,
|
|
1235
|
+
` ${sentinel}('__unknown__');`,
|
|
1236
|
+
``,
|
|
1237
|
+
` final String wire;`,
|
|
1238
|
+
` const ${name}(this.wire);`,
|
|
1239
|
+
``,
|
|
1240
|
+
` static ${name} fromWire(String wire) =>`,
|
|
1241
|
+
` ${name}.values.firstWhere((e) => e.wire == wire,`,
|
|
1242
|
+
` orElse: () => ${name}.${sentinel});`,
|
|
1243
|
+
`}`,
|
|
1244
|
+
].join("\n");
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
/** Every (fieldName, values) enum pair a VO descriptor carries (arrays recursed —
|
|
1248
|
+
* an array-of-enum field's element enum is named by the FIELD). */
|
|
1249
|
+
function voEnumFields(d: ValueObjectDescriptor): { fieldName: string; values: readonly string[] }[] {
|
|
1250
|
+
const out: { fieldName: string; values: readonly string[] }[] = [];
|
|
1251
|
+
const walk = (fieldName: string, f: ValueObjectFieldDescriptor): void => {
|
|
1252
|
+
if (f.type === "enum") out.push({ fieldName, values: f.values });
|
|
1253
|
+
else if (f.type === "array") walk(fieldName, f.items);
|
|
1254
|
+
};
|
|
1255
|
+
for (const [fieldName, f] of Object.entries(d.fields ?? {})) walk(fieldName, f);
|
|
1256
|
+
return out;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/** The Dart type of one VO descriptor field. `voName`/`fieldName` name the field's
|
|
1260
|
+
* generated enum type (`<VoName><FieldName>Enum`) — threaded through arrays. */
|
|
1261
|
+
function dartTypeOfVoField(f: ValueObjectFieldDescriptor, voName: string, fieldName: string): string {
|
|
1262
|
+
switch (f.type) {
|
|
1263
|
+
case "string":
|
|
1264
|
+
return "String";
|
|
1265
|
+
case "number":
|
|
1266
|
+
return "double";
|
|
1267
|
+
case "boolean":
|
|
1268
|
+
return "bool";
|
|
1269
|
+
case "enum":
|
|
1270
|
+
// A REAL generated Dart enum (`emitVoEnum`), named by the owning VO field.
|
|
1271
|
+
return voEnumName(voName, fieldName);
|
|
1272
|
+
case "array":
|
|
1273
|
+
return `List<${dartTypeOfVoField(f.items, voName, fieldName)}>`;
|
|
1274
|
+
case "object":
|
|
1275
|
+
case "geometry":
|
|
1276
|
+
return f.ref; // the named VO class (emitted alongside / imported)
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/** A Dart expression converting the RAW decoded json value `expr` to the field's typed
|
|
1281
|
+
* value. `depth` names nested lambda vars (`e0`, `e1`, …) for arrays-of-arrays. */
|
|
1282
|
+
function voFromExpr(
|
|
1283
|
+
f: ValueObjectFieldDescriptor,
|
|
1284
|
+
expr: string,
|
|
1285
|
+
voName: string,
|
|
1286
|
+
fieldName: string,
|
|
1287
|
+
depth = 0,
|
|
1288
|
+
): string {
|
|
1289
|
+
switch (f.type) {
|
|
1290
|
+
case "string":
|
|
1291
|
+
return `${expr} as String`;
|
|
1292
|
+
case "enum":
|
|
1293
|
+
// TOLERANT typed decode — an unknown wire value becomes the sentinel case.
|
|
1294
|
+
return `${voEnumName(voName, fieldName)}.fromWire(${expr} as String)`;
|
|
1295
|
+
case "number":
|
|
1296
|
+
return `(${expr} as num).toDouble()`;
|
|
1297
|
+
case "boolean":
|
|
1298
|
+
return `${expr} as bool`;
|
|
1299
|
+
case "array": {
|
|
1300
|
+
const v = `e${depth}`;
|
|
1301
|
+
return `(${expr} as List).map((${v}) => ${voFromExpr(f.items, v, voName, fieldName, depth + 1)}).toList()`;
|
|
1302
|
+
}
|
|
1303
|
+
case "object":
|
|
1304
|
+
case "geometry":
|
|
1305
|
+
return `${f.ref}.fromProjected((${expr} as Map).cast<String, Object?>())`;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
/** A Dart expression serializing the typed member `expr` back to plain json. */
|
|
1310
|
+
function voToJsonExpr(f: ValueObjectFieldDescriptor, expr: string, depth = 0): string {
|
|
1311
|
+
switch (f.type) {
|
|
1312
|
+
case "string":
|
|
1313
|
+
case "number":
|
|
1314
|
+
case "boolean":
|
|
1315
|
+
return expr;
|
|
1316
|
+
case "enum":
|
|
1317
|
+
return `${expr}.wire`; // the exact wire string the law's zod enum validates
|
|
1318
|
+
case "array": {
|
|
1319
|
+
const inner = voToJsonExpr(f.items, `e${depth}`, depth + 1);
|
|
1320
|
+
return inner === `e${depth}` ? expr : `${expr}.map((e${depth}) => ${inner}).toList()`;
|
|
1321
|
+
}
|
|
1322
|
+
case "object":
|
|
1323
|
+
case "geometry":
|
|
1324
|
+
return `${expr}.toJson()`;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/** Deep json equality/hash helpers shared by every generated VO class — emitted ONCE
|
|
1329
|
+
* per file when the file emits ≥1 VO class (`toJson()`-based value equality). */
|
|
1330
|
+
function emitVoHelpers(): string {
|
|
1331
|
+
return [
|
|
1332
|
+
`/// Deep json equality for generated value-object classes (toJson-based).`,
|
|
1333
|
+
`bool _voJsonEq(Object? a, Object? b) {`,
|
|
1334
|
+
` if (a is Map && b is Map) {`,
|
|
1335
|
+
` if (a.length != b.length) return false;`,
|
|
1336
|
+
` for (final k in a.keys) {`,
|
|
1337
|
+
` if (!b.containsKey(k) || !_voJsonEq(a[k], b[k])) return false;`,
|
|
1338
|
+
` }`,
|
|
1339
|
+
` return true;`,
|
|
1340
|
+
` }`,
|
|
1341
|
+
` if (a is List && b is List) {`,
|
|
1342
|
+
` if (a.length != b.length) return false;`,
|
|
1343
|
+
` for (var i = 0; i < a.length; i++) {`,
|
|
1344
|
+
` if (!_voJsonEq(a[i], b[i])) return false;`,
|
|
1345
|
+
` }`,
|
|
1346
|
+
` return true;`,
|
|
1347
|
+
` }`,
|
|
1348
|
+
` return a == b;`,
|
|
1349
|
+
`}`,
|
|
1350
|
+
``,
|
|
1351
|
+
`/// Deep json hash for generated value-object classes (toJson-based).`,
|
|
1352
|
+
`int _voJsonHash(Object? v) {`,
|
|
1353
|
+
` if (v is Map) {`,
|
|
1354
|
+
` return Object.hashAllUnordered(`,
|
|
1355
|
+
` v.entries.map((e) => Object.hash(e.key, _voJsonHash(e.value))));`,
|
|
1356
|
+
` }`,
|
|
1357
|
+
` if (v is List) return Object.hashAll(v.map(_voJsonHash));`,
|
|
1358
|
+
` return v.hashCode;`,
|
|
1359
|
+
`}`,
|
|
1360
|
+
].join("\n");
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
/** Emit ONE generated Dart value-object class from its canonical descriptor: typed
|
|
1364
|
+
* final fields, a const ctor, `fromProjected` (the projection's decoded json object),
|
|
1365
|
+
* `toJson` (the authored payload shape — optionals OMITTED when null, matching the zod
|
|
1366
|
+
* `.optional()` contract), and toJson-based value equality. A `geo.*` VO emits the
|
|
1367
|
+
* fixed RFC 7946 shape (typed `coordinates`; `type` is the constant geometry kind). */
|
|
1368
|
+
function emitValueObjectClass(d: ValueObjectDescriptor): string {
|
|
1369
|
+
if (d.geometry !== undefined) {
|
|
1370
|
+
const coordsType = geometryCoordsDart[d.geometry]!;
|
|
1371
|
+
const coordsFrom: Record<string, string> = {
|
|
1372
|
+
Point: `(m['coordinates']! as List).map((e0) => (e0 as num).toDouble()).toList()`,
|
|
1373
|
+
LineString: `(m['coordinates']! as List).map((e0) => (e0 as List).map((e1) => (e1 as num).toDouble()).toList()).toList()`,
|
|
1374
|
+
Polygon: `(m['coordinates']! as List).map((e0) => (e0 as List).map((e1) => (e1 as List).map((e2) => (e2 as num).toDouble()).toList()).toList()).toList()`,
|
|
1375
|
+
};
|
|
1376
|
+
return [
|
|
1377
|
+
`/// RFC 7946 ${d.geometry} — a \`geo.*\` value object ('${d.name}'). Positions are`,
|
|
1378
|
+
`/// \`[lng, lat]\` or \`[lng, lat, altitude]\` (RFC 7946 order).`,
|
|
1379
|
+
`class ${d.name} {`,
|
|
1380
|
+
` final ${coordsType} coordinates;`,
|
|
1381
|
+
``,
|
|
1382
|
+
` const ${d.name}({required this.coordinates});`,
|
|
1383
|
+
``,
|
|
1384
|
+
` /// The RFC 7946 geometry kind (constant).`,
|
|
1385
|
+
` String get type => '${d.geometry}';`,
|
|
1386
|
+
``,
|
|
1387
|
+
` factory ${d.name}.fromProjected(Map<String, Object?> m) =>`,
|
|
1388
|
+
` ${d.name}(coordinates: ${coordsFrom[d.geometry]!});`,
|
|
1389
|
+
``,
|
|
1390
|
+
` Map<String, Object?> toJson() => <String, Object?>{`,
|
|
1391
|
+
` 'type': '${d.geometry}',`,
|
|
1392
|
+
` 'coordinates': coordinates,`,
|
|
1393
|
+
` };`,
|
|
1394
|
+
``,
|
|
1395
|
+
` @override`,
|
|
1396
|
+
` bool operator ==(Object other) =>`,
|
|
1397
|
+
` other is ${d.name} && _voJsonEq(other.toJson(), toJson());`,
|
|
1398
|
+
``,
|
|
1399
|
+
` @override`,
|
|
1400
|
+
` int get hashCode => _voJsonHash(toJson());`,
|
|
1401
|
+
`}`,
|
|
1402
|
+
].join("\n");
|
|
1403
|
+
}
|
|
1404
|
+
const fields = Object.entries(d.fields ?? {}).map(([wireName, f]) => ({
|
|
1405
|
+
wireName,
|
|
1406
|
+
member: dartMemberName(wireName),
|
|
1407
|
+
f,
|
|
1408
|
+
optional: f.optional === true,
|
|
1409
|
+
}));
|
|
1410
|
+
const decls = fields.map(
|
|
1411
|
+
(x) => ` final ${dartTypeOfVoField(x.f, d.name, x.wireName)}${x.optional ? "?" : ""} ${x.member};`,
|
|
1412
|
+
);
|
|
1413
|
+
const ctor = fields.map((x) => (x.optional ? ` this.${x.member},` : ` required this.${x.member},`));
|
|
1414
|
+
const fromLines = fields.map((x) =>
|
|
1415
|
+
x.optional
|
|
1416
|
+
? ` ${x.member}: m['${x.wireName}'] == null ? null : ${voFromExpr(x.f, `m['${x.wireName}']!`, d.name, x.wireName)},`
|
|
1417
|
+
: ` ${x.member}: ${voFromExpr(x.f, `m['${x.wireName}']!`, d.name, x.wireName)},`,
|
|
1418
|
+
);
|
|
1419
|
+
const toLines = fields.map((x) =>
|
|
1420
|
+
x.optional
|
|
1421
|
+
? ` if (${x.member} != null) '${x.wireName}': ${voToJsonExpr(x.f, `${x.member}!`)},`
|
|
1422
|
+
: ` '${x.wireName}': ${voToJsonExpr(x.f, x.member)},`,
|
|
1423
|
+
);
|
|
1424
|
+
return [
|
|
1425
|
+
`/// Generated value object '${d.name}' (value_object.ts). Stored as ONE json-object`,
|
|
1426
|
+
`/// value leaf on its aggregate field — replaced WHOLESALE on write (Lww-class; no`,
|
|
1427
|
+
`/// per-subfield merge). Optional members are OMITTED from [toJson] when null (the`,
|
|
1428
|
+
`/// zod \`.optional()\` contract — absent, never JSON null).`,
|
|
1429
|
+
`class ${d.name} {`,
|
|
1430
|
+
...decls,
|
|
1431
|
+
``,
|
|
1432
|
+
` const ${d.name}({`,
|
|
1433
|
+
...ctor,
|
|
1434
|
+
` });`,
|
|
1435
|
+
``,
|
|
1436
|
+
` /// Read from the projection's decoded json object.`,
|
|
1437
|
+
` factory ${d.name}.fromProjected(Map<String, Object?> m) {`,
|
|
1438
|
+
` return ${d.name}(`,
|
|
1439
|
+
...fromLines,
|
|
1440
|
+
` );`,
|
|
1441
|
+
` }`,
|
|
1442
|
+
``,
|
|
1443
|
+
` /// The authored payload shape (embed it in a directive payload / \`set()\`).`,
|
|
1444
|
+
` Map<String, Object?> toJson() => <String, Object?>{`,
|
|
1445
|
+
...toLines,
|
|
1446
|
+
` };`,
|
|
1447
|
+
``,
|
|
1448
|
+
` @override`,
|
|
1449
|
+
` bool operator ==(Object other) =>`,
|
|
1450
|
+
` other is ${d.name} && _voJsonEq(other.toJson(), toJson());`,
|
|
1451
|
+
``,
|
|
1452
|
+
` @override`,
|
|
1453
|
+
` int get hashCode => _voJsonHash(toJson());`,
|
|
1454
|
+
`}`,
|
|
1455
|
+
].join("\n");
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1169
1458
|
/** True when ANY aggregate field in the module is `t.evidence()` — gates emitting the
|
|
1170
1459
|
* shared [emitEvidenceRefClass] once. */
|
|
1171
1460
|
function moduleUsesEvidence(aggregates: readonly AggregateHandle[]): boolean {
|
|
@@ -1235,8 +1524,15 @@ function parseField(s: AggFieldSpec, aggregate: string): string {
|
|
|
1235
1524
|
? `readStrOrNull(data, '${s.wireName}')`
|
|
1236
1525
|
: `readStr(data, '${aggregate}', '${s.wireName}')`;
|
|
1237
1526
|
case "json":
|
|
1238
|
-
// A
|
|
1239
|
-
//
|
|
1527
|
+
// A NAMED VALUE OBJECT reads the projection's decoded object and wraps it in its
|
|
1528
|
+
// generated class (the EvidenceRef pattern). A plain `json`-kind field is typed
|
|
1529
|
+
// `String` on the aggregate; the projection may have parsed its stored JSON into a
|
|
1530
|
+
// container — re-stringify to keep `String`.
|
|
1531
|
+
if (s.voClass !== undefined) {
|
|
1532
|
+
return opt
|
|
1533
|
+
? `() { final m = readJsonObjectOrNull(data, '${aggregate}', '${s.wireName}'); return m == null ? null : ${s.voClass}.fromProjected(m); }()`
|
|
1534
|
+
: `${s.voClass}.fromProjected(readJsonObject(data, '${aggregate}', '${s.wireName}'))`;
|
|
1535
|
+
}
|
|
1240
1536
|
return opt
|
|
1241
1537
|
? `readJsonStrOrNull(data, '${s.wireName}')`
|
|
1242
1538
|
: `readJsonStr(data, '${aggregate}', '${s.wireName}')`;
|
|
@@ -1304,6 +1600,8 @@ function readModelDartType(s: AggFieldSpec): string {
|
|
|
1304
1600
|
// has an UNKNOWN/non-object decoded shape (a double/bool/list authored as a JSON
|
|
1305
1601
|
// leaf — the kernel `Value` is Str|Int only), so it is surfaced as the permissive
|
|
1306
1602
|
// `Object?` (the read engine's decoded value as-is — never coerced/thrown).
|
|
1603
|
+
// A NAMED VALUE OBJECT reads as its generated class (typed decode).
|
|
1604
|
+
if (s.voClass !== undefined) return s.voClass;
|
|
1307
1605
|
return isJsonObjectLeaf(s) ? "Map<String, Object?>" : "Object?";
|
|
1308
1606
|
// `position`/`counter` are normalized to `string`/`int` in `aggFieldSpec` (read-collapsed scalars),
|
|
1309
1607
|
// so these labels are unreachable at runtime — present for FieldKind exhaustiveness.
|
|
@@ -1380,6 +1678,12 @@ function parseReadModelField(s: AggFieldSpec, aggregate: string): string {
|
|
|
1380
1678
|
// shape (a double/bool/list authored as a JSON leaf), so surface the read engine's
|
|
1381
1679
|
// decoded value AS-IS via the NON-THROWING `readJsonValueOrNull` (`Object?`, null
|
|
1382
1680
|
// when absent) — NEVER a strict-object reader that would throw on a legit non-object.
|
|
1681
|
+
if (s.voClass !== undefined) {
|
|
1682
|
+
// A NAMED VALUE OBJECT: the decoded object wrapped in its generated class.
|
|
1683
|
+
return opt
|
|
1684
|
+
? `() { final m = readJsonObjectOrNull(data, '${aggregate}', '${s.wireName}'); return m == null ? null : ${s.voClass}.fromProjected(m); }()`
|
|
1685
|
+
: `${s.voClass}.fromProjected(readJsonObject(data, '${aggregate}', '${s.wireName}'))`;
|
|
1686
|
+
}
|
|
1383
1687
|
if (isPermissiveJsonLeaf(s)) {
|
|
1384
1688
|
return `readJsonValueOrNull(data, '${s.wireName}')`;
|
|
1385
1689
|
}
|
|
@@ -2489,6 +2793,16 @@ export interface DomainModule {
|
|
|
2489
2793
|
* AST is hashed; the recursive Check/Expand EVALUATOR is the Rust engine substrate, never authored.
|
|
2490
2794
|
*/
|
|
2491
2795
|
relationTypes?: RelationTypeDecl[];
|
|
2796
|
+
/**
|
|
2797
|
+
* The domain's FIRST-CLASS VALUE OBJECTS (`value_object.ts`) — the deduped, sorted
|
|
2798
|
+
* canonical descriptors of every named VO the aggregates carry (transitive nested VOs
|
|
2799
|
+
* included; collected by `composeDomainModule`). ADDITIVE + OPTIONAL: a VO-free domain
|
|
2800
|
+
* omits this entirely (the `nomosValueObjects` era rule — package bytes unchanged).
|
|
2801
|
+
* NEVER in the canonical manifest/domainHash: a VO field's manifest bytes equal a plain
|
|
2802
|
+
* `t.jsonObject()` field's. Drives the generated TS interfaces + Dart classes and the
|
|
2803
|
+
* additive `nomosValueObjects` USD key (`usd.ts`).
|
|
2804
|
+
*/
|
|
2805
|
+
valueObjects?: readonly ValueObjectDescriptor[];
|
|
2492
2806
|
/**
|
|
2493
2807
|
* The domain's FIRST-CLASS WORKSPACE TYPES (`workspace_type.ts`; sharding §10
|
|
2494
2808
|
* RATIFIED, slice 1) — the law-declared workspace taxonomy
|
|
@@ -2598,6 +2912,13 @@ export interface GenerateDartDomainOptions {
|
|
|
2598
2912
|
* `import` so skipped framework symbols resolve. Omit when nothing is skipped.
|
|
2599
2913
|
*/
|
|
2600
2914
|
readonly frameworkImportUri?: string;
|
|
2915
|
+
/**
|
|
2916
|
+
* VALUE OBJECT names whose generated Dart class is emitted ONCE per package in the shared
|
|
2917
|
+
* `nomos_framework.dart` (a VO used by ≥2 domains) and MUST NOT be re-emitted here — this
|
|
2918
|
+
* domain references them via {@link frameworkImportUri}. Empty/absent ⇒ emit every VO the
|
|
2919
|
+
* module carries locally.
|
|
2920
|
+
*/
|
|
2921
|
+
readonly skipValueObjectNames?: ReadonlySet<string>;
|
|
2601
2922
|
}
|
|
2602
2923
|
|
|
2603
2924
|
/** Generate the full Dart source for one domain module. */
|
|
@@ -2606,6 +2927,22 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
|
|
|
2606
2927
|
const skipAggregateIds = opts.skipAggregateIds ?? new Set<string>();
|
|
2607
2928
|
const skipDirectiveIds = opts.skipDirectiveIds ?? new Set<string>();
|
|
2608
2929
|
const skipQueryIds = opts.skipQueryIds ?? new Set<string>();
|
|
2930
|
+
const skipValueObjectNames = opts.skipValueObjectNames ?? new Set<string>();
|
|
2931
|
+
// VALUE OBJECTS: one generated class per named VO the module carries (composeDomainModule
|
|
2932
|
+
// collected + sorted them), minus the package-shared ones (emitted once in the framework file).
|
|
2933
|
+
const localValueObjects = (mod.valueObjects ?? []).filter((d) => !skipValueObjectNames.has(d.name));
|
|
2934
|
+
// Each VO's block = its FIELD ENUMS (real Dart enums, tolerant fromWire) then its class —
|
|
2935
|
+
// so skipping a shared VO (the framework-file dedup) skips its enums with it.
|
|
2936
|
+
const voBlock =
|
|
2937
|
+
localValueObjects.length > 0
|
|
2938
|
+
? [
|
|
2939
|
+
emitVoHelpers(),
|
|
2940
|
+
...localValueObjects.flatMap((d) => [
|
|
2941
|
+
...voEnumFields(d).map((e) => emitVoEnum(d.name, e.fieldName, e.values)),
|
|
2942
|
+
emitValueObjectClass(d),
|
|
2943
|
+
]),
|
|
2944
|
+
].join("\n\n")
|
|
2945
|
+
: "";
|
|
2609
2946
|
// The symbols whose Dart surface this file EMITS (framework-shared ones live in the shared file).
|
|
2610
2947
|
const localAggregates = mod.aggregates.filter((a) => !skipAggregateIds.has(a.id));
|
|
2611
2948
|
const localDirectives = mod.directives.filter((d) => !skipDirectiveIds.has(d.id));
|
|
@@ -2841,6 +3178,13 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
|
|
|
2841
3178
|
``,
|
|
2842
3179
|
]
|
|
2843
3180
|
: []),
|
|
3181
|
+
...(voBlock !== ""
|
|
3182
|
+
? [
|
|
3183
|
+
`// ---- value objects (named composable json-object leaves; value_object.ts) ----`,
|
|
3184
|
+
voBlock,
|
|
3185
|
+
``,
|
|
3186
|
+
]
|
|
3187
|
+
: []),
|
|
2844
3188
|
`// ---- aggregates (projection types) ----`,
|
|
2845
3189
|
aggBlock,
|
|
2846
3190
|
``,
|
package/src/codegen_ts.ts
CHANGED
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
import type { z } from "zod";
|
|
38
38
|
|
|
39
39
|
import type { AggregateHandle } from "./aggregate.js";
|
|
40
|
-
import type { Field } from "./fields.js";
|
|
40
|
+
import type { Field, ValueObjectDescriptor, ValueObjectFieldDescriptor } from "./fields.js";
|
|
41
41
|
import type { Directive } from "./directive.js";
|
|
42
42
|
import type { DomainModule } from "./codegen_dart.js";
|
|
43
43
|
import type { QueryDecl } from "./query.js";
|
|
@@ -249,6 +249,9 @@ function tsTypeOfField(field: Field): string {
|
|
|
249
249
|
case "ref":
|
|
250
250
|
return "string"; // the related aggregate's id
|
|
251
251
|
case "json":
|
|
252
|
+
// A NAMED VALUE OBJECT field reads as its generated interface (`value_object.ts`);
|
|
253
|
+
// a bare `t.jsonObject()` stays the anonymous object; a plain `t.json()` is unknown.
|
|
254
|
+
if (field.valueObject !== undefined) return field.valueObject.name;
|
|
252
255
|
return field.jsonShape === "object" ? "Record<string, unknown>" : "unknown";
|
|
253
256
|
case "set":
|
|
254
257
|
return "string[]"; // `t.set(t.string())` is the one set shape (fields.ts)
|
|
@@ -264,6 +267,56 @@ function tsTypeOfField(field: Field): string {
|
|
|
264
267
|
}
|
|
265
268
|
}
|
|
266
269
|
|
|
270
|
+
// ── value objects → TS interfaces (`value_object.ts` descriptors) ─────────────────
|
|
271
|
+
|
|
272
|
+
/** The coordinates TS type per RFC 7946 geometry kind. */
|
|
273
|
+
const geometryCoordsTs: Record<string, string> = {
|
|
274
|
+
Point: "number[]",
|
|
275
|
+
LineString: "number[][]",
|
|
276
|
+
Polygon: "number[][][]",
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
/** One descriptor field → its TS type text. NO FALLBACK (the descriptor set is closed). */
|
|
280
|
+
function tsTypeOfVoField(f: ValueObjectFieldDescriptor): string {
|
|
281
|
+
switch (f.type) {
|
|
282
|
+
case "string":
|
|
283
|
+
return "string";
|
|
284
|
+
case "number":
|
|
285
|
+
return "number";
|
|
286
|
+
case "boolean":
|
|
287
|
+
return "boolean";
|
|
288
|
+
case "enum":
|
|
289
|
+
return union(f.values.map((v) => JSON.stringify(v)));
|
|
290
|
+
case "array":
|
|
291
|
+
return `${paren(tsTypeOfVoField(f.items))}[]`;
|
|
292
|
+
case "object":
|
|
293
|
+
case "geometry":
|
|
294
|
+
return f.ref; // the named VO interface (emitted alongside)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Emit one `export interface <VO> { ... }` from its canonical descriptor. A geo VO
|
|
299
|
+
* emits the fixed RFC 7946 shape (`{ type: "<kind>"; coordinates: ... }`). */
|
|
300
|
+
function emitValueObjectTs(d: ValueObjectDescriptor): string[] {
|
|
301
|
+
if (d.geometry !== undefined) {
|
|
302
|
+
return [
|
|
303
|
+
`/** RFC 7946 ${d.geometry} — a \`geo.${d.geometry === "Point" ? "point" : d.geometry === "LineString" ? "lineString" : "polygon"}(...)\` value object. */`,
|
|
304
|
+
`export interface ${d.name} { type: ${JSON.stringify(d.geometry)}; coordinates: ${geometryCoordsTs[d.geometry]}; }`,
|
|
305
|
+
``,
|
|
306
|
+
];
|
|
307
|
+
}
|
|
308
|
+
const lines = Object.entries(d.fields ?? {}).map(
|
|
309
|
+
([name, f]) => ` ${name}${f.optional === true ? "?" : ""}: ${tsTypeOfVoField(f)};`,
|
|
310
|
+
);
|
|
311
|
+
return [
|
|
312
|
+
`/** Value object \`${d.name}\` — ONE json-object value leaf, replaced WHOLESALE on write (no per-subfield merge). */`,
|
|
313
|
+
`export interface ${d.name} {`,
|
|
314
|
+
...lines,
|
|
315
|
+
`}`,
|
|
316
|
+
``,
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
|
|
267
320
|
/** A loose TS type for a derived/combined field's declared zod `returns`. */
|
|
268
321
|
function tsTypeOfReturns(returns: unknown): string {
|
|
269
322
|
try {
|
|
@@ -420,6 +473,26 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
420
473
|
);
|
|
421
474
|
}
|
|
422
475
|
}
|
|
476
|
+
// ── value objects: one interface per named VO, deduped across the package's domains ──
|
|
477
|
+
const voByName = new Map<string, ValueObjectDescriptor>();
|
|
478
|
+
for (const mod of modules) {
|
|
479
|
+
for (const d of (mod as { valueObjects?: readonly ValueObjectDescriptor[] }).valueObjects ?? []) {
|
|
480
|
+
const existing = voByName.get(d.name);
|
|
481
|
+
if (existing !== undefined && JSON.stringify(existing) !== JSON.stringify(d)) {
|
|
482
|
+
throw new Error(
|
|
483
|
+
`codegen-ts: value object '${d.name}' has DIVERGENT shapes across domains — one name, one shape.`,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
voByName.set(d.name, d);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (voByName.size > 0) {
|
|
490
|
+
out.push(`// ── value objects (named, composable json-object leaves — value_object.ts) ──`, ``);
|
|
491
|
+
for (const [, d] of [...voByName].sort(([a], [b]) => a.localeCompare(b))) {
|
|
492
|
+
out.push(...emitValueObjectTs(d));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
423
496
|
out.push(`// ── read models (projection \`data\` shapes; every field is partial-fold optional) ──`, ``);
|
|
424
497
|
for (const [typeId, agg] of [...aggByType].sort(([a], [b]) => a.localeCompare(b))) {
|
|
425
498
|
out.push(`export interface ${typeId}Data {`);
|
package/src/compile_engine.ts
CHANGED
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
* re-baking is a later, gated step. SYNTHETIC framework units only.
|
|
42
42
|
*/
|
|
43
43
|
import type { Directive } from "./directive.js";
|
|
44
|
+
import { isNamespacedAggregate, READ_PROJECTION_DELIMITER } from "./read_projection_namespace.js";
|
|
44
45
|
import type { AggregateHandle, AggregateInvariantFn } from "./aggregate.js";
|
|
45
46
|
import type { ComposeOptions } from "./compose.js";
|
|
46
47
|
import { flattenUsd, usdHash, type UsdDocument, type UsdPrim } from "./usd.js";
|
|
@@ -274,7 +275,17 @@ export function compileEngineModule(
|
|
|
274
275
|
`id "${directiveId}".`,
|
|
275
276
|
);
|
|
276
277
|
}
|
|
277
|
-
|
|
278
|
+
// READ-PROJECTION NAMESPACING: the USD law's target is the QUALIFIED type name for a
|
|
279
|
+
// non-global aggregate (`<domain>__<handleId>`); the executable binding keeps the bare
|
|
280
|
+
// handle id (the source literal). Accept either the bare id (global / pre-namespacing law)
|
|
281
|
+
// or the domain-qualified form (the prim's own layer domain — the 2nd path segment).
|
|
282
|
+
const aggGlobal = (binding.agg as { global?: boolean }).global;
|
|
283
|
+
const targetMatches = (aggId: string): boolean =>
|
|
284
|
+
prim.target === aggId ||
|
|
285
|
+
(isNamespacedAggregate(aggGlobal === undefined ? { id: aggId } : { id: aggId, global: aggGlobal }) &&
|
|
286
|
+
prim.target.endsWith(`${READ_PROJECTION_DELIMITER}${aggId}`) &&
|
|
287
|
+
prim.target.length > READ_PROJECTION_DELIMITER.length + aggId.length);
|
|
288
|
+
if (!targetMatches(binding.directive.aggregateId)) {
|
|
278
289
|
throw new EngineCompileError(
|
|
279
290
|
"binding-mismatch",
|
|
280
291
|
directiveId,
|
|
@@ -282,7 +293,7 @@ export function compileEngineModule(
|
|
|
282
293
|
`USD law's target is "${prim.target}".`,
|
|
283
294
|
);
|
|
284
295
|
}
|
|
285
|
-
if (binding.agg.id
|
|
296
|
+
if (!targetMatches(binding.agg.id)) {
|
|
286
297
|
throw new EngineCompileError(
|
|
287
298
|
"binding-mismatch",
|
|
288
299
|
directiveId,
|