@githolon/dsl 0.1.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/LICENSE.md +36 -0
- package/compile_package.mjs +50 -0
- package/package.json +59 -0
- package/src/aggregate.ts +167 -0
- package/src/authoring.ts +119 -0
- package/src/build_package.ts +636 -0
- package/src/certified_read.ts +313 -0
- package/src/codegen_dart.ts +2732 -0
- package/src/codegen_dot.ts +466 -0
- package/src/codegen_provider_dart.ts +358 -0
- package/src/codegen_ts.ts +365 -0
- package/src/codegen_usda.ts +388 -0
- package/src/combined.ts +195 -0
- package/src/compile_engine.ts +567 -0
- package/src/compile_package_main.ts +496 -0
- package/src/compose.ts +317 -0
- package/src/count.ts +218 -0
- package/src/ctx.ts +57 -0
- package/src/derived.ts +138 -0
- package/src/directive.ts +306 -0
- package/src/drivers.ts +95 -0
- package/src/emits_guard.ts +123 -0
- package/src/engine_entry.ts +449 -0
- package/src/exists.ts +170 -0
- package/src/extremum.ts +227 -0
- package/src/fields.ts +291 -0
- package/src/framework/bootstrap.ts +22 -0
- package/src/framework/disclosure.ts +108 -0
- package/src/framework/domain_lifecycle.ts +108 -0
- package/src/framework/identity.ts +537 -0
- package/src/framework/impure_capability.ts +643 -0
- package/src/framework/rbac.ts +418 -0
- package/src/framework/repair.ts +150 -0
- package/src/framework/sync_lifecycle.ts +125 -0
- package/src/framework/workspace_invariant.ts +128 -0
- package/src/framework/workspaces.ts +817 -0
- package/src/index.ts +317 -0
- package/src/manifest.ts +947 -0
- package/src/ops.ts +145 -0
- package/src/ordered_read.ts +228 -0
- package/src/predicate.ts +203 -0
- package/src/query/compile.ts +0 -0
- package/src/query/relations.ts +144 -0
- package/src/query.ts +151 -0
- package/src/read.ts +54 -0
- package/src/relation.ts +189 -0
- package/src/report/csv.ts +54 -0
- package/src/report.ts +401 -0
- package/src/spatial.ts +115 -0
- package/src/sum.ts +194 -0
- package/src/usd.ts +563 -0
- package/src/wire.ts +149 -0
- package/src/wire_encode.ts +250 -0
package/src/usd.ts
ADDED
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
|
+
// QuickJS engine} on V8 + WASI-shim, byte-identical everywhere. V8 = portability; the one
|
|
5
|
+
// wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
|
|
6
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* TS → USD — the UNIFIED composed-IR document (stage 1, increment 1).
|
|
10
|
+
*
|
|
11
|
+
* Two proven-but-isolated pieces become ONE here:
|
|
12
|
+
* - `manifest.ts` (#136): encodes a `DomainModule` to a canonical flat manifest —
|
|
13
|
+
* aggregates with field→Driver schemas + directive contracts (target / marker /
|
|
14
|
+
* requiresCapability / payload-shape), hashed via `canonicalJson` / `domainHash`.
|
|
15
|
+
* - `compose.ts` (#137): the typed, MONOTONIC layer composition — `requires`/`scope`
|
|
16
|
+
* rules, `covers()`, fail-closed `CompositionViolation`, composed hash.
|
|
17
|
+
*
|
|
18
|
+
* The bridge is an OpenUSD-shaped IR document: a real (synthetic) `DomainModule`
|
|
19
|
+
* encodes into composable typed prims arranged under a layer PATH; multiple layers
|
|
20
|
+
* COMPOSE via the very same typed rules (`compose.ts`'s `mergeRequiresScope`, reused
|
|
21
|
+
* — never re-implemented); the composed stage FLATTENS and HASHES. This is the
|
|
22
|
+
* foundation of the certified-IR document the gate will later seal.
|
|
23
|
+
*
|
|
24
|
+
* SYNTHETIC framework units only — paths like `/Nomos/Prelude` and `/Sample/...`;
|
|
25
|
+
* no tenant/business domain is named or imported. Build-time only (imports
|
|
26
|
+
* `node:crypto` transitively via `manifest.js`); reachable via the `@githolon/dsl/usd`
|
|
27
|
+
* subpath, NOT the runtime `index.ts` barrel.
|
|
28
|
+
*/
|
|
29
|
+
import { createHash } from "node:crypto";
|
|
30
|
+
import {
|
|
31
|
+
canonicalJson,
|
|
32
|
+
domainManifest,
|
|
33
|
+
type CanonicalProjectionReturn,
|
|
34
|
+
} from "./manifest.js";
|
|
35
|
+
import {
|
|
36
|
+
mergeRequiresScope,
|
|
37
|
+
CompositionViolation,
|
|
38
|
+
type ComposeOptions,
|
|
39
|
+
} from "./compose.js";
|
|
40
|
+
import type { WireSchema, WireRefMode } from "./wire.js";
|
|
41
|
+
import type { DomainModule } from "./codegen_dart.js";
|
|
42
|
+
|
|
43
|
+
/** The referential marker of a directive prim (kernel `RefMode` wire string). */
|
|
44
|
+
export type RefMode = WireRefMode;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* One prim in the USD IR — a discriminated union UNIFYING #136's manifest content
|
|
48
|
+
* with #137's composable typed-rule fields.
|
|
49
|
+
*
|
|
50
|
+
* - `aggregate`: an encoded aggregate, carrying its field→`WireDriver` schema (the
|
|
51
|
+
* EXACT `manifest.ts` `encodeKernelSchema` output). Aggregates do not compose-merge
|
|
52
|
+
* (an aggregate is a single-source schema); they pass through by path.
|
|
53
|
+
* - `directive`: a composable law unit. `requires` is the capability set (#137:
|
|
54
|
+
* add/narrow, never silently remove); `scope` is the optional composable scope
|
|
55
|
+
* path (#137: narrow-not-widen). `payloadRef` is the certified-artifact handle
|
|
56
|
+
* — left `undefined` here (stage 2).
|
|
57
|
+
*/
|
|
58
|
+
export type UsdPrim =
|
|
59
|
+
| {
|
|
60
|
+
readonly kind: "aggregate";
|
|
61
|
+
/** e.g. `/Sample/SampleThingAggregate`. */
|
|
62
|
+
readonly path: string;
|
|
63
|
+
/** The aggregate type id, e.g. `SampleThingAggregate`. */
|
|
64
|
+
readonly type: string;
|
|
65
|
+
/** The encoded field→Driver schema (reused from `manifest.ts`). */
|
|
66
|
+
readonly schema: WireSchema;
|
|
67
|
+
}
|
|
68
|
+
| {
|
|
69
|
+
readonly kind: "directive";
|
|
70
|
+
/** e.g. `/Sample/createThing`. */
|
|
71
|
+
readonly path: string;
|
|
72
|
+
/** The target aggregate type id. */
|
|
73
|
+
readonly target: string;
|
|
74
|
+
readonly marker: RefMode;
|
|
75
|
+
/** Capability ids that MUST hold (a stronger layer may only ADD). */
|
|
76
|
+
readonly requires: string[];
|
|
77
|
+
/**
|
|
78
|
+
* The composable scope path (#137 narrow-not-widen). `undefined` when the DSL
|
|
79
|
+
* declares NO static scope today — we do NOT fabricate one.
|
|
80
|
+
*/
|
|
81
|
+
readonly scope?: string;
|
|
82
|
+
/**
|
|
83
|
+
* The directive's DECLARED read boundary (sorted ref types) — the "IR for law
|
|
84
|
+
* boundaries" carried into the USD IR so `usdHash` reflects it. OMITTED
|
|
85
|
+
* (`undefined`) when the directive declares no reads, so boundary-free prims are
|
|
86
|
+
* byte-identical to before this field existed.
|
|
87
|
+
*/
|
|
88
|
+
readonly reads?: string[];
|
|
89
|
+
/**
|
|
90
|
+
* The directive's DECLARED emit boundary: event type → `{ max? }`. OMITTED
|
|
91
|
+
* (`undefined`) when no emits are declared (omit-when-empty hash invariance).
|
|
92
|
+
*/
|
|
93
|
+
readonly emits?: Record<string, { max?: number }>;
|
|
94
|
+
/**
|
|
95
|
+
* Standard JSON Schema for the directive payload, emitted from TS/Zod into the
|
|
96
|
+
* IR so cross-language tooling validates against the same schema source.
|
|
97
|
+
*/
|
|
98
|
+
readonly payloadJsonSchema?: unknown;
|
|
99
|
+
/** Certified-artifact payload reference — `undefined` for now (stage 2). */
|
|
100
|
+
readonly payloadRef?: string;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** One captured query declaration on a USD layer (read-side closure). `key` is the
|
|
104
|
+
* index column order, PRESERVED (not sorted). Mirrors `manifest.ts`'s `CanonicalQuery`. */
|
|
105
|
+
export interface UsdQuery {
|
|
106
|
+
readonly id: string;
|
|
107
|
+
readonly key: string[];
|
|
108
|
+
readonly returns: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** One captured derived read projection on a USD layer. */
|
|
112
|
+
export interface UsdDerived {
|
|
113
|
+
readonly id: string;
|
|
114
|
+
readonly of: string;
|
|
115
|
+
readonly returns: CanonicalProjectionReturn;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** One captured combined read projection on a USD layer. */
|
|
119
|
+
export interface UsdCombined {
|
|
120
|
+
readonly id: string;
|
|
121
|
+
readonly of: string;
|
|
122
|
+
readonly refField: string;
|
|
123
|
+
readonly reads: string;
|
|
124
|
+
readonly returns: CanonicalProjectionReturn;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** One USD layer: a stage path + its prims (canonically sorted by prim path). */
|
|
128
|
+
export interface UsdLayer {
|
|
129
|
+
readonly path: string;
|
|
130
|
+
readonly prims: UsdPrim[];
|
|
131
|
+
/**
|
|
132
|
+
* The layer's NAMED, INDEXED read declarations (read-side closure step 1), SORTED
|
|
133
|
+
* by id. OMITTED (`undefined`) when the module declares no query, so a query-free
|
|
134
|
+
* layer is byte-identical to before this field existed (omit-when-empty hash
|
|
135
|
+
* invariance). Carried straight from the module's canonical manifest.
|
|
136
|
+
*/
|
|
137
|
+
readonly queries?: UsdQuery[];
|
|
138
|
+
/** The layer's engine-projected derived read fields, sorted by id. */
|
|
139
|
+
readonly deriveds?: UsdDerived[];
|
|
140
|
+
/** The layer's engine-projected combined read fields, sorted by id. */
|
|
141
|
+
readonly combineds?: UsdCombined[];
|
|
142
|
+
/**
|
|
143
|
+
* The REFERENCE composition arc (LIVRPS: References sit just below Local). Paths of
|
|
144
|
+
* OTHER layers in the SAME document whose effective prims compose into THIS layer
|
|
145
|
+
* UNDER (weaker than) its own local prims — local is the stronger opinion. A
|
|
146
|
+
* first-class declared arc recorded in the IR so the composed identity (`usdHash`)
|
|
147
|
+
* reflects WHY a prim is present. OMITTED (`undefined`) when a layer references
|
|
148
|
+
* nothing, so a reference-free layer is byte-identical to before this field existed
|
|
149
|
+
* (omit-when-empty hash invariance — same discipline as `queries`/`reads`/`emits`).
|
|
150
|
+
*/
|
|
151
|
+
readonly references?: string[];
|
|
152
|
+
/**
|
|
153
|
+
* The VARIANT composition arc (LIVRPS: Variants sit ABOVE References, BELOW Local —
|
|
154
|
+
* stronger than referenced prims, weaker than this layer's own local prims). A layer
|
|
155
|
+
* may declare named variant SETS; each set is a family of named ALTERNATIVES, each
|
|
156
|
+
* alternative ("variant") holding its OWN prims. WHICH variant of each set composes is
|
|
157
|
+
* NOT baked into the document — it is supplied as `ComposeOptions.variantSelection`
|
|
158
|
+
* (setName → chosen variantName) at flatten time, so the composed identity is a
|
|
159
|
+
* function of `(doc, selection)`.
|
|
160
|
+
*
|
|
161
|
+
* Shape: setName → variantName → that variant's prims. A reserved variant name `"*"`
|
|
162
|
+
* within a set declares that set's DEFAULT variant: when the selection names no choice
|
|
163
|
+
* for the set, the `"*"` variant's prims compose; with neither a selection nor a `"*"`
|
|
164
|
+
* default, the set contributes NOTHING. A selection naming an unknown set or an unknown
|
|
165
|
+
* variant fails closed (`variant-unresolved`) — an explicit selection that cannot be
|
|
166
|
+
* resolved is never silently a no-op.
|
|
167
|
+
*
|
|
168
|
+
* OMITTED (`undefined`) when a layer declares no variant set, so a variant-free layer
|
|
169
|
+
* is byte-identical to before this field existed (omit-when-empty hash invariance —
|
|
170
|
+
* same discipline as `queries`/`references`/`reads`/`emits`).
|
|
171
|
+
*/
|
|
172
|
+
readonly variantSets?: Record<string, Record<string, UsdPrim[]>>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The reserved variant name declaring a set's DEFAULT (USD has a default variant). */
|
|
176
|
+
const DEFAULT_VARIANT = "*";
|
|
177
|
+
|
|
178
|
+
/** The unified USD IR document: ordered (strength) layers. */
|
|
179
|
+
export interface UsdDocument {
|
|
180
|
+
readonly layers: UsdLayer[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Sort prims by their `path` (canonical structure). */
|
|
184
|
+
function sortPrims(prims: UsdPrim[]): UsdPrim[] {
|
|
185
|
+
return [...prims].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Encode ONE `DomainModule` into the `UsdPrim`s sitting under `layerPath`. Reuses
|
|
190
|
+
* `manifest.ts`'s `domainManifest` (and therefore its `encodeKernelSchema` driver encoding)
|
|
191
|
+
* — no driver-encoding logic is duplicated here. Aggregate prims carry the exact
|
|
192
|
+
* encoded schema; directive prims map `requiresCapability` → `requires: [cap]`, leave
|
|
193
|
+
* `scope`/`payloadRef` `undefined` (no static scope today; payload is stage 2).
|
|
194
|
+
*/
|
|
195
|
+
function encodeModuleToPrims(layerPath: string, mod: DomainModule): UsdPrim[] {
|
|
196
|
+
const manifest = domainManifest(mod);
|
|
197
|
+
const prims: UsdPrim[] = [];
|
|
198
|
+
|
|
199
|
+
for (const agg of manifest.aggregates) {
|
|
200
|
+
prims.push({
|
|
201
|
+
kind: "aggregate",
|
|
202
|
+
path: `${layerPath}/${agg.id}`,
|
|
203
|
+
type: agg.id,
|
|
204
|
+
schema: agg.schema,
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const d of manifest.directives) {
|
|
209
|
+
prims.push({
|
|
210
|
+
kind: "directive",
|
|
211
|
+
path: `${layerPath}/${d.id}`,
|
|
212
|
+
target: d.target,
|
|
213
|
+
marker: d.marker,
|
|
214
|
+
requires: [d.requiresCapability],
|
|
215
|
+
// Carry the declared law boundary straight from the canonical manifest, which
|
|
216
|
+
// already applies omit-when-empty: `reads`/`emits` are present here ONLY when the
|
|
217
|
+
// directive declared them, so a boundary-free prim is byte-identical to before.
|
|
218
|
+
...(d.reads !== undefined ? { reads: d.reads } : {}),
|
|
219
|
+
...(d.emits !== undefined ? { emits: d.emits } : {}),
|
|
220
|
+
payloadJsonSchema: d.payloadJsonSchema,
|
|
221
|
+
// `scope`/`payloadRef` deliberately omitted (undefined): no static scope in
|
|
222
|
+
// the DSL today, payload is the stage-2 certified artifact.
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return sortPrims(prims);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Encode each `{ path, module }` into a `UsdLayer` of prims under that path, returning
|
|
231
|
+
* the canonical `UsdDocument`: layers SORTED by path, prims SORTED by path.
|
|
232
|
+
*/
|
|
233
|
+
export function emitUsd(
|
|
234
|
+
layers: { path: string; module: DomainModule }[],
|
|
235
|
+
): UsdDocument {
|
|
236
|
+
const out: UsdLayer[] = layers
|
|
237
|
+
.map((l) => {
|
|
238
|
+
// Carry the domain's declared queries straight from the canonical manifest,
|
|
239
|
+
// which already applies omit-when-empty: `queries` is present here ONLY when the
|
|
240
|
+
// module declared them, so a query-free layer is byte-identical to before.
|
|
241
|
+
const manifest = domainManifest(l.module);
|
|
242
|
+
return {
|
|
243
|
+
path: l.path,
|
|
244
|
+
prims: encodeModuleToPrims(l.path, l.module),
|
|
245
|
+
...(manifest.queries !== undefined ? { queries: manifest.queries } : {}),
|
|
246
|
+
...(manifest.deriveds !== undefined ? { deriveds: manifest.deriveds } : {}),
|
|
247
|
+
...(manifest.combineds !== undefined ? { combineds: manifest.combineds } : {}),
|
|
248
|
+
};
|
|
249
|
+
})
|
|
250
|
+
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
251
|
+
return { layers: out };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Two aggregate prims at the same path are identical iff same type + same schema. */
|
|
255
|
+
function aggregateEqual(
|
|
256
|
+
a: Extract<UsdPrim, { kind: "aggregate" }>,
|
|
257
|
+
b: Extract<UsdPrim, { kind: "aggregate" }>,
|
|
258
|
+
): boolean {
|
|
259
|
+
return a.type === b.type && canonicalJson(a.schema) === canonicalJson(b.schema);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Merge a stronger `over` prim onto a `base` prim at the SAME path, per the #137
|
|
264
|
+
* typed rules — reusing `compose.ts`'s `mergeRequiresScope` for directive prims
|
|
265
|
+
* (requires add/narrow-not-remove; scope narrow-not-widen; widen/remove need
|
|
266
|
+
* `authority`). An uncertified payload would always be refused — but `payloadRef`
|
|
267
|
+
* is unset here (stage 2), so there is nothing to refuse yet.
|
|
268
|
+
*
|
|
269
|
+
* Mixing prim KINDS at one path, or redefining an aggregate's schema across layers,
|
|
270
|
+
* is not a typed monotonic move — it is a structural conflict, surfaced fail-closed.
|
|
271
|
+
*/
|
|
272
|
+
function mergeUsdPrim(base: UsdPrim, over: UsdPrim, opts: ComposeOptions): UsdPrim {
|
|
273
|
+
if (base.kind !== over.kind) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`USD composition conflict at ${over.path}: cannot merge a ${base.kind} prim ` +
|
|
276
|
+
`with a ${over.kind} prim.`,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (base.kind === "aggregate" && over.kind === "aggregate") {
|
|
281
|
+
// Aggregates are single-source schemas: an identical re-declaration is a no-op
|
|
282
|
+
// pass-through; a divergent one is a structural conflict (not a monotonic move).
|
|
283
|
+
if (!aggregateEqual(base, over)) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`USD composition conflict at ${over.path}: aggregate schema redefined ` +
|
|
286
|
+
`across layers.`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return over;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Both directive prims — apply the shared typed requires+scope rule core (#137).
|
|
293
|
+
if (base.kind === "directive" && over.kind === "directive") {
|
|
294
|
+
if (base.target !== over.target) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`USD composition conflict at ${over.path}: directive target changed from ` +
|
|
297
|
+
`"${base.target}" to "${over.target}".`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (base.marker !== over.marker) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`USD composition conflict at ${over.path}: directive marker changed from ` +
|
|
303
|
+
`"${base.marker}" to "${over.marker}".`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
if (
|
|
307
|
+
base.payloadJsonSchema !== undefined &&
|
|
308
|
+
over.payloadJsonSchema !== undefined &&
|
|
309
|
+
canonicalJson(base.payloadJsonSchema) !== canonicalJson(over.payloadJsonSchema)
|
|
310
|
+
) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`USD composition conflict at ${over.path}: directive payload schema changed.`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
const merged = mergeRequiresScope(over.path, base, over, opts);
|
|
316
|
+
const out: Extract<UsdPrim, { kind: "directive" }> = {
|
|
317
|
+
kind: "directive",
|
|
318
|
+
path: over.path,
|
|
319
|
+
target: over.target,
|
|
320
|
+
marker: over.marker,
|
|
321
|
+
requires: merged.requires,
|
|
322
|
+
...(over.payloadJsonSchema !== undefined
|
|
323
|
+
? { payloadJsonSchema: over.payloadJsonSchema }
|
|
324
|
+
: base.payloadJsonSchema !== undefined
|
|
325
|
+
? { payloadJsonSchema: base.payloadJsonSchema }
|
|
326
|
+
: {}),
|
|
327
|
+
...(merged.scope !== undefined ? { scope: merged.scope } : {}),
|
|
328
|
+
// Preserve the declared law boundary across a monotonic merge (omit-when-empty):
|
|
329
|
+
// the stronger layer's declaration wins when present, else the base's carries
|
|
330
|
+
// through; a boundary-free pair stays byte-identical to before this field existed.
|
|
331
|
+
...(over.reads !== undefined
|
|
332
|
+
? { reads: over.reads }
|
|
333
|
+
: base.reads !== undefined
|
|
334
|
+
? { reads: base.reads }
|
|
335
|
+
: {}),
|
|
336
|
+
...(over.emits !== undefined
|
|
337
|
+
? { emits: over.emits }
|
|
338
|
+
: base.emits !== undefined
|
|
339
|
+
? { emits: base.emits }
|
|
340
|
+
: {}),
|
|
341
|
+
// payloadRef stays unset (stage 2).
|
|
342
|
+
};
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Unreachable (kinds proven equal above) — exhaustiveness guard.
|
|
347
|
+
throw new Error(`USD composition: unhandled prim kind at ${over.path}.`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Canonicalize a standalone prim — sort a directive's `requires`. */
|
|
351
|
+
function canonicalizeStandalone(prim: UsdPrim): UsdPrim {
|
|
352
|
+
if (prim.kind === "directive") {
|
|
353
|
+
return { ...prim, requires: [...prim.requires].sort() };
|
|
354
|
+
}
|
|
355
|
+
return prim;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Fold a stream of prims (already in weak→strong order) into an effective prim map
|
|
360
|
+
* keyed by path: a path's first contributor passes through (canonicalized); each
|
|
361
|
+
* later (stronger) contributor MERGES per the #137 typed rules (fail-closed
|
|
362
|
+
* `CompositionViolation` on any rule it cannot satisfy).
|
|
363
|
+
*/
|
|
364
|
+
function foldPrims(
|
|
365
|
+
acc: Map<string, UsdPrim>,
|
|
366
|
+
prims: Iterable<UsdPrim>,
|
|
367
|
+
opts: ComposeOptions,
|
|
368
|
+
): void {
|
|
369
|
+
for (const prim of prims) {
|
|
370
|
+
const existing = acc.get(prim.path);
|
|
371
|
+
if (existing === undefined) {
|
|
372
|
+
acc.set(prim.path, canonicalizeStandalone(prim));
|
|
373
|
+
} else {
|
|
374
|
+
acc.set(prim.path, mergeUsdPrim(existing, prim, opts));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Resolve ONE layer's EFFECTIVE prims (the REFERENCE arc, LIVRPS: References below
|
|
381
|
+
* Local). Each referenced layer's effective prims are composed FIRST (weaker base),
|
|
382
|
+
* in declared `references[]` order, then THIS layer's own LOCAL prims fold OVER them
|
|
383
|
+
* (stronger) via the same typed `mergeUsdPrim`. Resolution is deterministic and
|
|
384
|
+
* DETECTS CYCLES (A→B→A) via the `stack` of layer paths currently being resolved,
|
|
385
|
+
* failing closed with a `CompositionViolation`. `byPath` indexes the document's
|
|
386
|
+
* layers; a reference to an unknown layer path also fails closed.
|
|
387
|
+
*
|
|
388
|
+
* VARIANTS (LIVRPS: Variants sit ABOVE References, BELOW Local) resolve AFTER the
|
|
389
|
+
* reference fold and BEFORE the local fold: the SELECTED variant of each declared set
|
|
390
|
+
* (or its `"*"` default, else nothing) folds OVER the referenced base (variants are
|
|
391
|
+
* stronger than references) and UNDER the local prims (local is the strongest), all
|
|
392
|
+
* through the same monotonic `mergeUsdPrim`. The selection is `opts.variantSelection`,
|
|
393
|
+
* an explicit flatten input — so the composed identity reflects it. A selection naming
|
|
394
|
+
* an unknown set or variant fails closed (`variant-unresolved`).
|
|
395
|
+
*/
|
|
396
|
+
function resolveLayerEffective(
|
|
397
|
+
layer: UsdLayer,
|
|
398
|
+
byPath: Map<string, UsdLayer>,
|
|
399
|
+
opts: ComposeOptions,
|
|
400
|
+
stack: string[],
|
|
401
|
+
): UsdPrim[] {
|
|
402
|
+
if (stack.includes(layer.path)) {
|
|
403
|
+
throw new CompositionViolation(
|
|
404
|
+
"reference-cycle",
|
|
405
|
+
layer.path,
|
|
406
|
+
`reference cycle detected: ${[...stack, layer.path].join(" -> ")}`,
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const acc = new Map<string, UsdPrim>();
|
|
411
|
+
|
|
412
|
+
// References first (weaker), in declared order — each resolved recursively so
|
|
413
|
+
// transitive references compose and cycles are caught at any depth.
|
|
414
|
+
if (layer.references !== undefined) {
|
|
415
|
+
const nextStack = [...stack, layer.path];
|
|
416
|
+
for (const refPath of layer.references) {
|
|
417
|
+
const refLayer = byPath.get(refPath);
|
|
418
|
+
if (refLayer === undefined) {
|
|
419
|
+
throw new CompositionViolation(
|
|
420
|
+
"reference-unresolved",
|
|
421
|
+
layer.path,
|
|
422
|
+
`references unknown layer path "${refPath}"`,
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
foldPrims(acc, resolveLayerEffective(refLayer, byPath, opts, nextStack), opts);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Variants fold OVER references, UNDER local (LIVRPS variant strength). The selected
|
|
430
|
+
// variant of each declared set — or its `"*"` default — contributes its prims; an
|
|
431
|
+
// unselected, default-less set contributes nothing. A selection for an unknown set or
|
|
432
|
+
// an unknown variant fails closed.
|
|
433
|
+
foldPrims(acc, selectedVariantPrims(layer, opts), opts);
|
|
434
|
+
|
|
435
|
+
// Local prims fold OVER the referenced + variant base (strongest opinion wins).
|
|
436
|
+
foldPrims(acc, layer.prims, opts);
|
|
437
|
+
|
|
438
|
+
return [...acc.keys()].sort().map((path) => acc.get(path)!);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* The prims contributed by THIS layer's variant sets under the active selection
|
|
443
|
+
* (`opts.variantSelection`). For each declared set, in canonical (sorted) set-name
|
|
444
|
+
* order: if the selection names a variant, that variant's prims are used (unknown set
|
|
445
|
+
* or unknown variant → `variant-unresolved`, fail-closed); otherwise the set's `"*"`
|
|
446
|
+
* DEFAULT variant is used when declared, else the set contributes nothing. The
|
|
447
|
+
* selection is also validated against the layer's declared sets so a selection naming
|
|
448
|
+
* a set this layer does not declare fails closed too.
|
|
449
|
+
*
|
|
450
|
+
* Returns the contributed prims in canonical order (set-name, then prim-path) so the
|
|
451
|
+
* fold input is deterministic — the fold itself is order-stable for non-overlapping
|
|
452
|
+
* paths, and any overlap within the variant arc is resolved by the same monotonic rule.
|
|
453
|
+
*/
|
|
454
|
+
function selectedVariantPrims(layer: UsdLayer, opts: ComposeOptions): UsdPrim[] {
|
|
455
|
+
const sets = layer.variantSets;
|
|
456
|
+
const selection = opts.variantSelection ?? {};
|
|
457
|
+
|
|
458
|
+
if (sets === undefined) return [];
|
|
459
|
+
|
|
460
|
+
const out: UsdPrim[] = [];
|
|
461
|
+
for (const setName of Object.keys(sets).sort()) {
|
|
462
|
+
const variants = sets[setName]!;
|
|
463
|
+
const chosen = selection[setName];
|
|
464
|
+
let variantPrims: UsdPrim[] | undefined;
|
|
465
|
+
if (chosen !== undefined) {
|
|
466
|
+
// A selection naming a variant THIS layer's set does not hold fails closed — an
|
|
467
|
+
// explicit choice that cannot bind to a declared alternative is never silently
|
|
468
|
+
// dropped to the default / nothing.
|
|
469
|
+
variantPrims = variants[chosen];
|
|
470
|
+
if (variantPrims === undefined) {
|
|
471
|
+
throw new CompositionViolation(
|
|
472
|
+
"variant-unresolved",
|
|
473
|
+
layer.path,
|
|
474
|
+
`variant set "${setName}" has no variant named "${chosen}"`,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
} else {
|
|
478
|
+
// No explicit choice: the declared default variant, else nothing.
|
|
479
|
+
variantPrims = variants[DEFAULT_VARIANT];
|
|
480
|
+
}
|
|
481
|
+
if (variantPrims !== undefined) {
|
|
482
|
+
for (const prim of sortPrims(variantPrims)) out.push(prim);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return out;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Validate the WHOLE selection against the document: each selected set name MUST be
|
|
490
|
+
* declared by at least ONE layer. A selection naming a set NO layer declares fails
|
|
491
|
+
* closed (`variant-unresolved`) — an explicit choice that can never bind is never a
|
|
492
|
+
* silent no-op. (The per-variant check — an unknown variant WITHIN a declared set —
|
|
493
|
+
* is enforced per-layer in `selectedVariantPrims`, where the set is in scope.) This is
|
|
494
|
+
* document-scoped because the selection is a single global input to the flatten, while
|
|
495
|
+
* a given set may be declared on only some of the document's layers.
|
|
496
|
+
*/
|
|
497
|
+
function assertSelectionResolvable(doc: UsdDocument, opts: ComposeOptions): void {
|
|
498
|
+
const selection = opts.variantSelection;
|
|
499
|
+
if (selection === undefined) return;
|
|
500
|
+
const declared = new Set<string>();
|
|
501
|
+
for (const layer of doc.layers) {
|
|
502
|
+
if (layer.variantSets !== undefined) {
|
|
503
|
+
for (const setName of Object.keys(layer.variantSets)) declared.add(setName);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
for (const setName of Object.keys(selection)) {
|
|
507
|
+
if (!declared.has(setName)) {
|
|
508
|
+
throw new CompositionViolation(
|
|
509
|
+
"variant-unresolved",
|
|
510
|
+
setName,
|
|
511
|
+
`selection names variant set "${setName}" not declared by any layer`,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Fold the document's layers IN ORDER into the effective flattened prim set.
|
|
519
|
+
*
|
|
520
|
+
* Per layer, the REFERENCE arc resolves FIRST (weakest), then the VARIANT arc (the
|
|
521
|
+
* selected variant of each declared set per `opts.variantSelection`, stronger than
|
|
522
|
+
* references, weaker than local), then this layer's LOCAL prims (strongest) fold OVER
|
|
523
|
+
* both (LIVRPS), producing the layer's effective prim set; resolution is deterministic
|
|
524
|
+
* and fails closed on cycles / unknown reference paths / unresolved variant selections.
|
|
525
|
+
* Then the existing SUBLAYER fold composes those per-layer
|
|
526
|
+
* effective prim sets in `layers[]` (strength) order, unchanged: a prim present in
|
|
527
|
+
* only one layer passes through; a prim a later layer contributes at a path an earlier
|
|
528
|
+
* layer already defined is MERGED per the #137 typed rules (fail-closed
|
|
529
|
+
* `CompositionViolation`). Returns the prim set SORTED by path.
|
|
530
|
+
*/
|
|
531
|
+
export function flattenUsd(
|
|
532
|
+
doc: UsdDocument,
|
|
533
|
+
opts: ComposeOptions = {},
|
|
534
|
+
): UsdPrim[] {
|
|
535
|
+
// Fail closed on a selection naming a set NO layer declares (document-scoped — the
|
|
536
|
+
// selection is a single global input; a set may live on only some layers).
|
|
537
|
+
assertSelectionResolvable(doc, opts);
|
|
538
|
+
|
|
539
|
+
const byPath = new Map<string, UsdLayer>();
|
|
540
|
+
for (const layer of doc.layers) byPath.set(layer.path, layer);
|
|
541
|
+
|
|
542
|
+
const acc = new Map<string, UsdPrim>();
|
|
543
|
+
|
|
544
|
+
for (const layer of doc.layers) {
|
|
545
|
+
const effective = resolveLayerEffective(layer, byPath, opts, []);
|
|
546
|
+
foldPrims(acc, effective, opts);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return [...acc.keys()]
|
|
550
|
+
.sort()
|
|
551
|
+
.map((path) => acc.get(path)!);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* The composed-IR identity: sha256 hex of `canonicalJson(flattenUsd(doc, opts))`.
|
|
556
|
+
* Extends #136 (the manifest hash) to a flattened MULTI-LAYER stage, reusing the
|
|
557
|
+
* same byte-stable canonicalizer.
|
|
558
|
+
*/
|
|
559
|
+
export function usdHash(doc: UsdDocument, opts: ComposeOptions = {}): string {
|
|
560
|
+
return createHash("sha256")
|
|
561
|
+
.update(canonicalJson(flattenUsd(doc, opts)), "utf8")
|
|
562
|
+
.digest("hex");
|
|
563
|
+
}
|