openusd 0.5.0

Rust native USD library
Documentation
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is a pure Rust implementation of OpenUSD (Universal Scene Description), Pixar's open-source framework for 3D computer graphics data interchange. The project aims to provide native Rust support without C++ dependencies for reading and writing USD files.

## Architecture

The codebase follows the same module structure as the C++ OpenUSD SDK:

- **`sdf/`** - Scene Description Foundations: Core data types, traits, and abstractions. Contains the `AbstractData` trait, `Value` enum (60+ variants, including the dedicated `asset` / `asset[]` payloads `AssetPath` = `Value::AssetPath` / `Value::AssetPathVec`), the `AssetPath` value type (C++ `SdfAssetPath`: an authored path plus the evaluated and resolved paths that value resolution fills in — `expressionVariables` substitution then resolver anchoring against the strongest opinion's layer, in `IndexCache`; identity is authored-path-only; string-like via `Deref`/`AsRef`/`Borrow`/`PartialEq<str>`, and fetchable through `Attribute::get::<sdf::AssetPath>()`), `Path`, `Spec`, `ListOp`, and schema field keys. `Layer` (C++ `SdfLayer`) bundles a resolved identifier with a backing `AbstractData` and exposes a Layer-tier authoring surface (`create_prim` / `override_prim` / `create_attribute` / `create_relationship` / `set_default_prim` / `clear_default_prim`, plus `relocates` / `set_relocates` / `has_relocates` / `clear_relocates` mirroring `SdfLayer::GetRelocates` and friends over the `sdf::Relocate` = `(Path, Path)` and `sdf::RelocateList` = `Vec<Relocate>` aliases) plus typed spec views (`PrimSpec[Mut]`, `AttributeSpec[Mut]`, `RelationshipSpec[Mut]`, `PseudoRootSpec[Mut]`). Authoring errors flow through `AuthoringError`.

- **`usda/`** - Text format (`.usda`): Lexer (logos) + recursive descent parser. `TextReader` implements `AbstractData`; `TextWriter` emits `.usda`.

- **`usdc/`** - Binary format (`.usdc`): USD's crate format with compressed data handling. `CrateData` implements `AbstractData`; `CrateWriter` emits `.usdc`.

- **`usdz/`** - Archive format (`.usdz`): ZIP-based package. `Archive` reads and `ArchiveWriter` emits `.usdz`.

- **`ar/`** - Asset Resolution: `Resolver` trait maps asset paths (`@...@`) to physical locations. `DefaultResolver` searches the filesystem.

- **`layer`** - Layer collection: `Collector::collect` recursively resolves and loads all layers from a root file, following sublayers, references, and payloads. Corresponds to C++ `PcpLayerStack`. `Collector` and `DependencyKind` are re-exported from the crate root. Defines `layer::Error` for recoverable collection failures (e.g. unresolved assets).

- **`pcp/`** - Prim Cache Population (composition engine): Implements LIVRPS strength ordering to compose opinions across layers. Corresponds to C++ `Pcp` module.
  - `pcp/mod.rs` — `pcp::Error`: composition errors (arc cycles, unresolved layers, missing/invalid `defaultPrim`, `InvalidExpression` for a reference/payload asset-path variable expression that fails to evaluate, and `ArcPermissionDenied` for a direct arc to a `permission = private` site). All are recoverable — the broken arc is skipped and recorded while the rest of the prim composes. `VariantFallbackMap`: maps variant set names to ordered fallback selections (C++ `PcpVariantFallbackMap`).
  - `pcp/layer_graph.rs` — `LayerGraph` (C++ `PcpLayerStack`, the layer-ownership half): the loaded layers and their sublayer DAG, keyed by `pcp::LayerId` (a cheap `Copy` handle the graph mints by interning each layer's identifier — not a content hash, so no collision; `bare sdf::Layer` carries no id, and a `by_identifier` map resolves the public identifier-string surface to a handle in O(1)), with a `LayerNode` (layer + sublayer-edge list + validated relocates) per id and a precomputed sublayer stack per layer (its DFS pre-order with offsets composed), rebuilt whenever the edges change. Sublayer edges are always derived from each layer's `subLayers`/`subLayerOffsets` metadata — the single source of truth — by `recompute_sublayers`; both an ordinary `subLayers` authoring edit and the public `Stage::insert_sub_layer` / `remove_sub_layer` route through it (`ensure_layer` adds the new node, the parent's metadata is authored, then the change pipeline rebuilds), so a sublayer added via the API persists on save and a later metadata edit never wipes it. The graph owns its regenerable diagnostics (`cycle_errors` from `build_sublayer_edges`, `relocate_errors` from `recompute_relocates`), each replaced wholesale on rebuild and read through `LayerGraph::errors` — so a fixed cycle/relocate stops being reported and a recompute never duplicates one. `Stage` holds it in a `RefCell` separate from the `IndexCache`, borrowing each independently; composition reads it through a shared `&LayerGraph` parameter, keeping every per-prim build a pure function of `(graph, context, cached indices)`.
  - `pcp/index_cache.rs` — `IndexCache`: lazily-built per-prim composition cache (C++ `PcpCache`). Borrows the stage's `LayerGraph` by shared reference for each build. `IndexCache::process_changes` is the single entry point for surgical invalidation — Stage feeds `(LayerId, sdf::ChangeList)` pairs and the cache classifies + applies. Operational failures return `Result`; recoverable per-prim `pcp::Error` diagnostics plus the one-shot collection errors (e.g. `UnresolvedSublayer`) are retained in `composition_errors`, and `Stage::composition_errors()` concatenates them with the graph's `errors()`. `ensure_index` also enforces arc permissions: `detect_arc_permissions` finds each direct composition arc whose target site is `permission = private` (spec 10.3.3), then `PrimIndex::mark_permission_denied_under` marks every node reached through a denied target path `PERMISSION_DENIED` (the C++ `_AddArc` + `_InertSubtree` behavior) so it stops contributing to value resolution while staying visible structurally. Denied target paths flow down `CompositionContext::denied_prefixes` so descendant prims composed separately (where the arc is extended, not authored) are inerted too.
  - `pcp/instancing.rs` — scene-graph instancing (spec 11.3.3). `PrototypeRegistry` is the encapsulated object (owned as one `IndexCache` field) that maps each `InstanceKey` — the arc-introduced opinions defining an instance's subtree, with variant selections folded in explicitly (each arc path is stripped of its selections and the resolved `(set, selection)` set is carried alongside, so two instances of one reference share iff their selections match) — to its `/__Prototype_N`, dedups by key (signalling on `register` whether it minted a new prototype), and answers prototype/instance queries; it holds no composition state. The composition-coupled glue stays as a second `impl IndexCache` here. The prototype *namespace* is the single composition of a set of identical instances and composes in place: the root is materialized as an independent index (`materialize_prototype` — a clone of the canonical instance's index with its instance-local nodes inerted (`PrimIndex::mark_instance_local_inert`) and the composed-namespace side of every node's maps re-anchored onto `/__Prototype_N` (`PrimIndex::rebase_root` / `MapFunction::rebase_target`; node site paths stay in their layer namespace so value resolution reads the real specs, including variant opinions authored at the instance's own path), cached through `IndexCache::cache_index`), and a prototype descendant composes by deepening that graph through the ordinary `ensure_index` path. Every instance proxy redirects *onto* the prototype namespace: the `effective_path` redirection every descendant query enters through (`redirect_anchor`) maps an instance proxy `/A/tail` to `/__Prototype_N/tail` (the instance root composes in place — spec 11.3.3 lets it override property values), so identical instances compose once and proxy prim stacks report the shared layer-namespace spec paths the goldens expect. The per-prim redirection is memoized in the `redirected_prims` field (identity result included, so a non-instanced prim skips the ancestor walk too), cleared whenever the registry is invalidated. Because the prototype namespace composes in place, a nested instance inside it (`/__Prototype_N/.../NestedInstance`) is a real instance that mints its own prototype, and a prim beneath it is an instance proxy of that nested prototype; relationship/connection targets resolve in the prototype namespace and `compose_property_paths` remaps them back to the queried instance for a proxy (the prototype root's own targets already land in the prototype namespace via the rebased map). `is_instance`, `prototype_of`, `is_instance_proxy` / `prim_in_prototype` (C++ `UsdPrim::IsInstanceProxy` / `GetPrimInPrototype`, both existence-checked), the `instance_key` computation, and `enclosing_instance` round out the glue, delegating dedup to the registry and reading the cache's `indices` (`pub(super)`) via `ensure_index`. A composition change drops only the affected prototypes through `invalidate_prototypes` (`PrototypeRegistry::remove_affected`: a prototype whose instance subtree or `/__Prototype_N` root lies on a changed prim path's chain, dropping each affected root's whole subtree), or the whole registry through `invalidate_all_prototypes` on a layer-stack rebuild; `prototype_root_of` resolves a path's enclosing prototype root so `Stage::mask_includes` can populate prototype content whenever a sharing instance is in the population mask.
  - `pcp/change.rs` — `Changes` (`pub(crate)`), `CacheChanges`, `LayerStackChanges`: C++ `PcpChanges` analog. Three-tier classifier (significant / prim / spec) feeds `Changes::did_change` (pure analysis) and `Changes::apply` (commit). External callers go through `IndexCache::process_changes`, which ties both phases to the same cache instance.
  - `pcp/dependencies.rs` — `Dependencies`: reverse `(LayerId, site_path) → prim_index_paths` map populated at the end of `IndexCache::ensure_index`. Drives `lookup_with_ancestors` and `subtree_lookup` for change fanout. Synthetic self-registrations on every layer make cached misses (empty `PrimIndex`) reachable.
  - `pcp/prim_index.rs`, `pcp/prim_graph.rs`, `pcp/prim_resolve.rs` — per-prim composition tree (C++ `PcpPrimIndex` / `PcpNodeRef`), split across three files: `prim_graph.rs` owns the graph types (`Node`, `NodeId`, `NodeFlags`, `ArcType`, `PrimIndexGraph`) and the strength-order projection; `prim_resolve.rs` owns value resolution over a composed `PrimIndex` (`PrimIndex::opinions()` and the `resolve_*` field walks); `prim_index.rs` hosts the `PrimIndex` struct with its `build_with_cache` / `build_with_cache_in` entry points, `CompositionContext`, `AncestorArc`, and arc-composition helpers (`compose_references_in`, `collect_payloads_in`, etc.) — the build itself delegates entirely to `Indexer` in `pcp/prim_indexer.rs`. `ArcType` variants are ordered by LIVERPS strength via `#[repr(u8)]` + derived `Ord`. `PrimIndexGraph` stores nodes in an append-only arena (`NodeId` handles index it and stay stable for the life of the index — no `Rc`/`RefCell`, so the whole index is `Send + Sync`) plus a separate `strength_order` projection; value resolution walks `PrimIndex::nodes()` (the projection, strongest first), while the indexer reads `arena()` in structural order. Each node represents a `(layerStack, path)` site (C++ `PcpNode`): it holds a `layer_stack` of `(LayerId, sublayer offset)` members, strongest first, plus a `has_specs` flag (C++ `PcpNode::HasSpecs`) recording whether any member authors a spec at its path — a cloned ancestral site deepened to a child with no opinion there is kept with `has_specs == false` for structure (it may still introduce arcs) but contributes nothing to value resolution. `node.layer_id()` is the representative (strongest) member, and `is_empty` / `has_composition_arc` / the prim-stack dump exclude `has_specs == false` nodes. Value resolution iterates `node.layers()` and skips non-authoring layers naturally (each member's effective offset is `map_to_root`'s arc offset with the sublayer offset composed on top). Reading a reference/payload from a member folds that member's sublayer offset into the arc offset (`compose_references_in` / `collect_payloads_in`, C++ `PcpComposeSiteReferences`). A reference/payload whose target authors no spec is kept as a `CULLED` node carrying the target layer stack: `PrimIndex::all_nodes()` surfaces it (and dependency registration covers its layers) so an editor and change tracking see the arc, while value resolution (`nodes()`), `is_empty`, and `has_composition_arc` skip it. A direct arc to a `permission = private` site (and any node reached through it, in this prim or a descendant) is marked `PERMISSION_DENIED` (C++ `_InertSubtree`): `nodes()` / `has_spec` / child names keep it for structural visibility, but `PrimIndex::opinions()` (the single value-resolution chokepoint) skips it, so its opinions never win. Each node also carries `parent`/`children`/`origin` tree links, a `NodeFlags` bitset (reserving the C++ `PcpNodeRef` state surface; `INERT`, `CULLED`, `PERMISSION_DENIED`, `HAS_SPECIALIZES`, `IMPLIED_CLASS`, and `RELOCATE_SOURCE` are set today), `namespace_depth` / `specialize_chain_depth` for strength comparison, and `map_to_parent` / `map_to_root` (`MapFunction`) for namespace translation and the arc-level retiming (`MapFunction` carries an `sdf::LayerOffset`) across arcs. The graph is a single rooted tree under a synthetic, inert root (`NodeFlags::INERT`) that owns every otherwise-parentless node (root `L` site, ancestor arcs, implied classes, relocate inserts); value resolution skips it, and re-parenting under its identity mapping is offset-neutral. `finalize_strength_order` builds the projection as a pre-order DFS of strength-ordered children (`compare_sibling_node_strength` / `compare_node_strength`: arc type, namespace depth, origin strength, sibling number — the `GetSiblingNumAtOrigin` key). The specializes copies live under the local root as the weakest arc (gated on the `specializes_propagated` flag the indexer sets), so the DFS already places that globally-weak band (spec 10.4.1) last; they are ordered among themselves by `compare_specialize_siblings` (C++ `PcpCompareSiblingNodeStrength`'s specializes branch: origin-root namespace depth unless `_OriginsAreNestedArcs`, origin-chain length, then origin strength). Relocate nodes, grafted after the build, splice into the finalized projection.
  - `pcp/prim_indexer.rs` — `Indexer` (C++ `Pcp_PrimIndexer`): the sole composition path. Grows the graph node-by-node by draining a priority task queue, mirroring C++ `Pcp_BuildPrimIndex`. Reference/payload diamonds (a shared target reached by two arc paths) contribute a node on each path. Ancestral opinions enter through the graph-clone seed (C++ `_BuildInitialPrimIndexFromAncestor`): a child prim clones its parent's already-composed graph and deepens every site path by the child name (`PrimIndexGraph::append_child_name_to_all_sites`), so the references and payloads an ancestor introduced re-evaluate at the deepened path through the same queue. Inherits compose as class-based arcs (C++ `_EvalNodeInherits` → `_AddClassBasedArc`), and a class brought in through a reference is mirrored into the referencing namespace by implied-class propagation (C++ `_EvalImpliedClasses` → `_EvalImpliedClassTree`): an `EvalImpliedClasses` task carries a node's class-based children one level up toward the root, repeating up the arc chain; `Node` carries `sibling_num_at_origin` / `depth_below_introduction` / `path_at_introduction` (C++ `PcpNode`) for the class-hierarchy strength and start-node logic, and a non-contributing implied placeholder is marked `INERT`. An arc whose target is not a root prim is composed as its own sub-index (including the target's ancestral opinions) and grafted under the arc node (C++ `_AddArc`'s `includeAncestralOpinions` branch → `InsertChildSubgraph`, ported as `Indexer::compose_subindex` / `graft_subindex`); the ancestral parent reuses the frame chain (`compose_ancestral_subindex`, C++ `_BuildInitialPrimIndexFromAncestor`'s else-branch). A borrowed `Frame` chain (C++ `PcpPrimIndex_StackFrame`) threads the parent graphs back through nested builds so duplicate-node skipping (`Indexer::find_duplicate`, C++ `_AddArc`'s `skipDuplicateNodes` search with `ReplacePrefix` across frames) drops a class reached both directly and through an ancestral reference, grafting it once. Variant sets resolve through a second band of queue tasks (`EvalNodeVariantSets` → `EvalNodeVariantAuthored` → `EvalNodeVariantFallback` → `EvalNodeVariantNoneFound`, C++ `_EvalNodeVariantSets` and friends), weaker than every arc task so the strongest variant selection is known before a variant grafts; the authored/fallback tasks break ties by live node strength, so the queue is drained by `Indexer::take_best_task` (a graph-aware comparator over a `Vec<Task>`) rather than a fixed `Ord`. Selecting a variant grafts the `{set=sel}` site (identity-stripping map) and re-enqueues its arc and nested-variant tasks, then retries pending fallback tasks (C++ `RetryVariantTasks`); a set with no authored or configured-fallback selection stays unselected (C++ `_EvalNodeFallbackVariant`; no implicit first-variant default). A nested sub-build skips variants (C++ `evaluateVariantsAndDynamicPayloads == false`), so a grafted sub-index's local variant sets and the variant sets authored above each grafted node are re-evaluated at the top level through a parallel ancestral band (`EvalNodeAncestralVariantSets` → `EvalNodeAncestralVariantAuthored` → … , C++ `_EvalNodeAncestralVariantSets` / `_AddAncestralVariantArc`), stronger than the local variant band but weaker than implied classes. The selection is composed by `Indexer::compose_variant_selection` (C++ `_ComposeVariantSelection`): the set path is first translated to the strongest node it namespace-maps to (`translate_path_to_root_or_closest`, C++ `Pcp_TranslatePathFromNodeToRootOrClosestNode`), then that node's subtree is searched strongest-first, so a stronger frame's selection wins only where the mapping reaches it. Specializes compose as globally-weak class-based arcs (C++ `_EvalNodeSpecializes` / `_EvalImpliedSpecializes`): a specializes node is added as an inert placeholder where authored and copied under the local root (`_PropagateNodeToRoot`, `Indexer::propagate_node_to_root`); a specializes authored across a reference is carried to every namespace level by the implied-class mechanism (specializes are class-based, so `eval_implied_class_tree` reads the propagated copy's children) and each level copied to the root. Relocates are handled by `eval_node_relocations`: a relocation source is composed as a sub-index and grafted, with the prohibited-prim elision for salted earth. The indexer takes only `&` references (Rayon-friendly — see the `TODO(rayon)` at the cross-prim driver in `index_cache.rs`). The root `L` site scans only the build's ambient layer stack (the stage root layer stack, or a referenced asset's sublayer stack for an arc target) — opinions from other layer stacks arrive solely by following their arcs. Known gaps (tracked in the `pcp` module docs): variant-gated and cross-arc implied relocations (`eval_implied_relocations`), specializes/inherit authored inside a selected variant, and relationship/connection target remapping through relocates.
  - `pcp/mapping.rs` — `MapFunction`: namespace mapping between composition arcs, a port of C++ `PcpMapFunction`. Stores explicit (source, target) path pairs plus a `has_root_identity` flag (the `/ → /` catch-all, kept as a flag — not a pair — matching C++) and an `sdf::LayerOffset` time retiming. A pair with an empty target is a *block*: it makes its source subtree unmappable (C++ `_Map` returning the empty path), which is how a relocate to a prim outside an outer arc's domain stops the pre-relocation path from mapping. `map_source_to_target` / `map_target_to_source` do longest-prefix matching; `translate_to_target` adds the bidirectional-invertibility check (C++ `_Map`'s `_HasBetterTargetMatch`): a result shadowed by a closer inverse match is dropped, which is what makes a class connection target naming the class's own instance image fail to translate and keeps a relocated class target at its pre-relocation path. `MapFunction::new` / `from_parts` canonicalize (C++ `PcpMapFunction::Create` / `_Canonicalize`), folding a `(/, /)` pair into the flag and dropping pairs a shorter-prefix entry already implies so the reverse lookup stays unambiguous. `compose` is the two-step C++ `Compose`: each inner pair carried through `self` (an escaped target becoming a block), each `self` pair pulled back through inner, root identity present iff both have it. `without_variant_selections` strips variant selections from the pairs (C++ keeps variant arc maps identity, the selection only in the node path) and is applied by `PrimIndex::map_to_root_for_targets` before target translation. `node.map_to_root.time_offset()` is the node's arc-level offset; a contributing layer's effective offset folds its sublayer offset on top (`node.layers()`).
  - `pcp/relocates.rs` — stateless relocate free functions (no owning type): extract and validate per-layer `layerRelocates` (`validate_layer_relocates`), compute effective relocates in composed namespace (`effective_relocates` / `effective_and_escaped`), and resolve pre-relocation source paths. The per-node child-name relocate classification (rename in place / remove / cross-hierarchy add, plus prohibited names) is `apply_child_relocates`, the port of C++ `_ComposePrimChildNamesAtNode` that `IndexCache::compute_prim_child_names` drives against each node's `LayerGraph::combined_relocates` (chained within the node's layer stack, so a same-parent chain `A→B`, `B→C` resolves `A` straight to `C`) while folding child names weak-to-strong. Each relocate node carries the source site's full layer stack (`Node::new_site` / `graft_relocate_node` take a `Vec<(LayerId, LayerOffset)>`), so a relocate source spanning several sublayers keeps every member. Every function reads the validated relocates straight off the `LayerGraph`; all external data (`&LayerGraph`, cached indices/contexts) is passed through parameters, and nothing references `IndexCache` directly.
  - `pcp/clip.rs` — `ClipSet`: value clips (spec 12.3.4). Parses the explicit `clips` / `clipSets` metadata model and maps stage time to clip time during attribute value resolution. Template clips are resolved to explicit metadata elsewhere.

- **`usd/`** - Composed stage API: `usd::Stage` provides the high-level API for opening USD files and querying the composed scene graph. Delegates composition to `pcp::IndexCache`. `Stage` is a cheap reference-counted handle (`Rc<StageInner>`, mirroring C++ `UsdStageRefPtr`): cloning bumps the refcount, and all interior mutability lives in per-field cells on the crate-internal `StageInner`, reached through a `Deref`. `StageBuilder` configures the stage with a custom resolver, variant fallback selections (`variant_fallbacks`), an optional session layer (`session_layer`), payload loading behavior (`load` / `InitialLoadSet`), and a working-set filter (`mask` / `StagePopulationMask`). Recoverable composition errors are collected during the build and read back through `Stage::composition_errors` rather than a callback. `StageBuilder::open` loads a root layer from disk; `StageBuilder::in_memory` creates an empty writable anonymous root (C++ `UsdStage::CreateInMemory`). Stage-tier authoring (`define_prim` / `override_prim` / `create_attribute` / `create_relationship` / `set_default_prim`) routes through the current `EditTarget` (a subset of C++ `UsdEditTarget`): it pairs a layer identifier string (resolved to a `pcp::LayerId` handle at author time) with a `pcp::MapFunction` that maps scene paths to spec paths, so authoring through the chokepoint `Stage::with_target_layer_at` writes at `EditTarget::map_to_spec_path`. Sublayer mutation (`insert_sub_layer` / `remove_sub_layer`), muting (`mute_layer`), and `sub_layers` likewise address layers by identifier; composition introspection (the `pcp::PrimIndex` nodes) is what hands back the opaque `pcp::LayerId` handle, resolved with `Stage::layer_identifier`. `EditTarget::for_layer` is the identity-mapping local target; `EditTarget::for_local_direct_variant` routes writes into a variant's `{set=sel}` namespace, which `sdf::Layer` materializes end-to-end (`create_prim` / `create_attribute` accept variant-segment paths and build the variant set / variant spec scaffolding via `namespace_chain`; arc-based reference/specialize targets are still TODO). `EditContext` is the RAII guard (C++ `UsdEditContext`) returned by `Stage::edit_context` that restores the previous target on `Drop`. Each authoring closure returns an `sdf::ChangeList` (in layer namespace) that `IndexCache::process_changes` classifies and applies for surgical invalidation. Errors surface via `StageAuthoringError` (including `OutsideEditTarget` when a path can't be mapped). `usd/prim.rs` defines the `Prim` / `VariantSets` composed handles (C++ `UsdPrim` / `UsdVariantSets` analogs) and `usd/attribute.rs` / `usd/relationship.rs` the `Attribute` / `Relationship` handles (C++ `UsdAttribute` / `UsdRelationship`) — value-type wrappers around `(stage, path)` that own a `Stage` clone (no lifetime parameter), so they are `Clone` and can be stored independently of the call that produced them. `usd/schema.rs` defines `usd::SchemaBase` (C++ `UsdSchemaBase`), the root trait of the schema-view hierarchy: a view wraps a `Prim` and exposes `prim()` / `path()` / `stage()`, declares its `KIND` (`usd::SchemaKind`, mirroring `UsdSchemaKind`) for the `is_concrete` / `is_typed` / `is_*_api_schema` classification queries, and unwraps back to its `Prim` via `into_prim` (with a blanket `From<S: SchemaBase> for Prim`). The `schemas/` domain views are migrating onto it through intermediate schema traits; every schema family — `schemas::geom`, `schemas::lux`, `schemas::media`, `schemas::proc`, `schemas::vol`, `schemas::physics`, `schemas::ui`, `schemas::render`, `schemas::shade`, and `schemas::skel` — has fully moved (geom: a `SchemaBase → Imageable → Xformable → Boundable → Gprim → PointBased → Curves` trait chain with concrete newtype views like `Mesh` / `Camera` / `Sphere`; lux, media, proc, and vol build on it — lux lights, `media::SpatialAudio`, `proc::GenerativeProcedural`, and `vol::Volume` (a geom `Gprim`, with its field prims as geom `Xformable`) are geom prims, and `lux` / `media` / `proc` / `vol` enable the `geom` feature for that reuse; applied API schemas like `LightAPI` / `ShapingAPI` / `media::AssetPreviewsAPI` are single-apply views; `physics` stands apart from geom — typed `Scene` / `Joint` prims plus single- and multiple-apply API schemas, and the first family with multi-apply views (`DriveAPI` / `LimitAPI`, a `{prim, name}` newtype keyed by a DOF instance name, with `Get` / `GetAll`); `ui` is a small no-geom family — the typed `Backdrop` prim plus the single-apply `SceneGraphPrimAPI` / `NodeGraphNodeAPI`; `render` pairs the `RenderSettingsBase` interface trait (shared camera + framing attrs) with the typed `RenderSettings` / `RenderProduct` / `RenderVar` / `RenderPass` / `RenderDenoisePass`, and keeps the computed render spec (`compute_render_spec`, reading through the views); `shade` is connection-topology rather than flat attrs — the `Connectable` interface trait (`inputs:` / `outputs:` accessors plus `connectability` / `renderType` metadata and `Attribute::connect_to`, the general single-source connection helper) backs the typed `Shader` / `NodeGraph` / `Material`, the single-apply `MaterialBindingAPI` keeps the direct/collection binding-resolution machinery, and `Material::compute_surface_source` + `read_preview_surface` stay as connection-following computation; `UsdShadeInput`/`Output` are exposed as raw `Attribute` handles rather than distinct value types; `skel` is the only geom-derived family besides geom itself with a kept computation toolkit — `SkelRoot` / `Skeleton` are geom `Boundable` prims, `SkelAnimation` / `BlendShape` are typed, `SkelBindingAPI` is single-apply, and the time-independent object model (`Topology`, `AnimMapper`, `SkeletonResolver`, `SkinningResolver`, `SkelAnimQuery`, `discover_bindings`, the LBS / blend-shape math) is rewired to build from the views via decoded getters like `Skeleton::joints()` / `bind_transforms()`). All ten replace the old freestanding `read_*` / `*Author` functions; each concrete view declares its chain via an `impl_<family>_schema!` macro and exposes C++-style `foo_attr()` / `create_foo_attr()` handle pairs. A migrated family is laid out as `mod.rs` (re-exports + the macro + token-valued enums), `tokens.rs`, `schema.rs` (the view structs, grouped — geom splits these by sub-family), and an optional `traits.rs` (the interface traits); the shared view-gate helpers (`get_typed` / `get_typed_any` / `get_with_api`) and the `impl_token_value!` macro (gives a token enum `From`/`TryFrom<Value>` for direct `set(Enum)` / `get::<Enum>()`) live in `schemas::common`, while attribute authoring is inlined via the `Attribute` indexer (`create_attribute(name, type).set_custom(false)`). The registry-backed members (`GetSchemaClassPrimDefinition`, `GetSchemaAttributeNames`) await the `schemas::registry`. Setters consume `self` and return `Self` so chains bind directly (`let p = stage.define_prim(...)?.set_type_name(...)?.set_kind(...)?`); lookup-style methods (`create_attribute`, `path`, `get`, `time_samples`) take `&self`. Composition introspection mirrors the C++ handle surface: `Prim::prim_stack` (`UsdPrim::GetPrimStack`), `Attribute`/`Relationship::property_stack` (`UsdProperty::GetPropertyStack`), `Prim::variant_sets().get_all_variant_selections()` (`UsdVariantSets::GetAllVariantSelections`), plus the stage-scoped `Stage::layer_stack` (`UsdStage::GetLayerStack`) and the handle factories `Stage::prim_at` / `attribute_at` / `relationship_at` (`UsdStage::GetPrimAtPath` / `GetAttributeAtPath` / `GetRelationshipAtPath`); these return `(layer identifier, spec path)` sites where C++ returns spec handles. `Prim::prim_index` (`UsdPrim::GetPrimIndex`) returns a `PrimIndexRef` — a cheap `(stage, path)` handle whose queries borrow the cache briefly and collect recoverable errors into `composition_errors`; through it `graph` clones the composed `pcp::PrimIndex` and `child_names` returns `(children, prohibited)` (`PcpPrimIndex::ComputePrimChildNames`). The composed scene queries live on the handles: `Prim::children` / `property_names` / `attributes` / `relationships` (`UsdPrim::GetChildren` / `GetPropertyNames` / `GetAttributes` / `GetRelationships`), `Relationship::targets` / `forwarded_targets` (`UsdRelationship::GetTargets` / `GetForwardedTargets`), `Attribute::connections` (`UsdAttribute::GetConnections`), and `Relationship::compute_targets` / `Attribute::compute_connections` returning `(targets, deleted)`, the per-property target index (`PcpBuildFilteredTargetIndex`'s paths plus its `deletedPaths` out-param). They reach the cache through the `Stage` query infrastructure — `Stage::with_cache` (run a cache query, with recoverable per-prim errors collected into `composition_errors`), `Stage::masked` (the same gated on the population mask via `Stage::mask_includes`, which also populates a `/__Prototype_N` path when a sharing instance is in the mask), `Stage::filter_child_names`, and the crate-internal composed-spec primitives `Stage::has_spec` / `spec_type` (the post-composition analog of `SdfAbstractData::HasSpec` / `GetSpecType`, behind `Prim::is_valid` and the attribute/relationship split) — none of which are public. `Stage::layer_identifier` resolves a composition node's `pcp::LayerId` to its identifier. Stage-level interpolation lives in `usd/interp.rs` and is exposed as `usd::InterpolationType`. Public users should import modules, e.g. `use openusd::{sdf, usd};`, rather than root-level `Stage` re-exports.

- **`sdf/expr.rs`** - Variable expression tokenizer, parser, and evaluator for USD's `` `...` `` expression syntax (C++ `SdfVariableExpression`), re-exported as `sdf::expr` with `sdf::Expr`. Composing the `expressionVariables` an expression evaluates against across reference/payload arcs (the referencing layer stack overriding the referenced one) happens both during layer collection (`layer::Collector` threads them parent→child so the loaded set matches what composition resolves) and in the `pcp` indexer (`composed_expr_vars`), C++ `PcpExpressionVariables`.

- **`schemas/`** - Domain-schema readers/authoring layered on top of the core `sdf` / `usd` machinery (the AOUSD core spec covers composition, value resolution, and file formats — not these schemas). Each sub-module is feature-gated: `geom` (UsdGeom), `physics` (UsdPhysics), `skel` (UsdSkel + skinning), `lux` (UsdLux), `shade` (UsdShade), `render` (UsdRender). `schemas/registry.rs` is the eventual schema-registry surface (currently a stub).

- **`gf/`** - Graphics Foundations: vector, quaternion, and matrix types mirroring
  the C++ `Gf` namespace (`GfVec*`, `GfQuat*`, `GfMatrix*`). All types are
  `#[repr(C)]` / `#[repr(transparent)]` + `bytemuck::Pod` so they can be
  bulk-cast for binary serialization. `Vec2/3/4` in `f32` (`f`), `f64` (`d`),
  `f16` (`h`), and `i32` (`i`) variants; `Quatf/d/h` with `(w, x, y, z)` field
  order and `slerp`; `Mat2d`, `Mat3d`, `Matrix4d` (row-major, row-vector
  convention). `gf::f16` re-exports `half::f16`. Free-function constructors
  (`vec3f`, `quatf`, …) and `lerp` / `lerp_half` / `slerp` live in `gf/mod.rs`.
  `From<gf::T> for sdf::Value` and `TryFrom<sdf::Value> for gf::T` cover every
  gf type so schema code can convert without boilerplate.

The `AbstractData` trait in `sdf/mod.rs` serves as the central abstraction, providing a unified interface for text, binary, and archive format readers.

## Development Commands

```bash
# Build the project (use --all-features to include the gated schema modules)
cargo build --all-features

# Run tests (including comprehensive format validation tests)
cargo test --all-targets --all-features

# Lint with Clippy (strict warnings as errors)
cargo clippy --all-targets --all-features -- -D warnings

# Format code
cargo fmt

# Check formatting
cargo fmt --all -- --check --files-with-diff

# Generate documentation
cargo doc --no-deps

# Run security/dependency audits
cargo deny check

# Run examples
cargo run --example dump_usdc -- path/to/file.usd
```

## Planning New Features

When implementing a new feature from the spec:

1. Read `docs/aousd_core_spec_1.0.1.pdf` to understand how the feature works and what it does
2. Research how the C++ OpenUSD implementation handles it: https://github.com/PixarAnimationStudios/OpenUSD
3. Review the Python reference implementation if applicable: `vendor/core-spec-supplemental-release_dec2025/`

## Code Standards

- Project targets Rust version specified in `rust-toolchain.toml` with MSRV defined in `clippy.toml`
- Maximum line width: 120 characters (rustfmt.toml)
- All warnings treated as errors in CI
- Comprehensive test coverage (50% minimum) with grcov
- Security auditing with cargo-deny
- Pre-1.0: backward compatibility is not a constraint. Prefer the cleanest design and change or remove public APIs freely; don't keep deprecated shims, compatibility shims, or worse-but-compatible behavior. Update all call sites in the same change.

## Code Quality

- Write clean and idiomatic Rust code
- Less is better - prefer functionality offered by stdlib
- Order a file top-down by importance so the first thing a reader sees is the main type, not a helper: the primary type definition (and the structs it depends on) first, then its `impl` blocks, then free-standing helper functions, then the `#[cfg(test)] mod tests`. Don't open a file with a small private helper.
- Code requires documentation
- Proof read and reword docs and/or comments as needed
- Do not use `**bold** — description` pattern in doc comments or bullet lists; use plain text or link directly to the item instead
- A doc comment documents only its own item. Don't describe another type, module, or method inside it (e.g. don't enumerate a `Layer`'s methods in a `Relocate` type alias's doc); document each item on the item itself and use an intra-doc link (`` [`Foo`] ``) when a cross-reference is genuinely needed
- Do not use decorative box-drawing section-divider comments (e.g. `// ── Section ──────`); group code with a plain `//` comment or rely on the item's own doc comment
- Never remove comments during refactoring if they are still applicable
- Comments must describe the code as it stands, not its edit history or the alternatives it didn't take. Don't justify the absence or removal of code, and don't contrast the chosen approach with a rejected one (e.g. "no separate X pre-check is needed here", "X was removed because…", "assign directly rather than through Y", "instead of calling Z", "we use A so we don't B"). Such notes only make sense to someone who saw the prior version or the alternative and are noise to a fresh reader. State what the present code does and a rationale that stands on its own, not what it no longer does or could have done.
- Don't reference planning phases or steps (e.g. "Phase 1", "Step 2") in code, comments, names, fixtures, or commit messages; describe what the code does or, for deferred work, name the missing feature in a `TODO`
- Wrap prose at 80 characters — Markdown, plans, design write-ups, and doc-comment text; Rust code still follows rustfmt (120)
- Mark performance/parallelism opportunities with a `TODO(rayon)` (or `TODO(perf)`) comment in new code and when refactoring existing code, instead of optimizing prematurely; say what is independent or parallelizable so the seam is actionable later
- Re-export key types from module roots so users can access them without deep paths (e.g. `sdf::FieldKey` not `sdf::schema::FieldKey`)
- Avoid raw path string manipulations; use `Path` methods instead of building or parsing path strings manually
- Don't add "Generated with Claude Code" or "Co-Authored-By: Claude" to commits, PRs, or release notes

## Testing

The test suite includes extensive binary format tests using fixture files in `fixtures/` directory. Tests validate:
- Data type parsing (integers, floats, strings, arrays, etc.)
- USD-specific types (paths, references, payloads, layer offsets)
- Compression handling
- Time-sampled data
- Scene hierarchy traversal

Prefer using USD assets from `vendor/usd-wg-assets/` for test fixtures when a suitable file exists. Only add new files to `fixtures/` when vendor assets don't cover the specific case needed.

Test function names MUST be terse — 2–4 underscore-separated words, no more. Match the existing naming convention of the file. Prefer `add_api_schema_dup_delete` over `add_api_schema_clears_duplicate_delete_opinions`, `light_api_skips_non_light` over `read_light_api_returns_none_on_non_light_prim`. Drop redundant prefixes like `read_`/`reads_` when the test file already targets a reader; favour the subject + outcome (`light_api_via_applied_schema`) over verbose sentences.

To pull a typed payload out of a `Value` in tests, use the `EnumTryAs`-generated `try_as_*()` accessor followed by `unwrap()`/`expect("…")` rather than a `let Value::Variant(x) = … else { panic!() }` block. Keep the descriptive message by preferring `expect`.

## Dependencies

Key external dependencies:
- `anyhow` - Error handling
- `bitflags` - Bitflag sets (e.g. `PrimPredicate`)
- `bytemuck` - Safe transmutation for binary data
- `half` - 16-bit floating point support (re-exported as `f16`)
- `logos` - Lexer generator for USDA tokenization
- `lz4_flex` - Compression for binary format
- `num-traits` - Numeric traits
- `strum` - Enum derive macros (Display, EnumIs, EnumTryAs, IntoStaticStr, etc.)
- `thiserror` - Error type derive macros for typed errors such as `layer::Error` and `pcp::Error`
- `zip` - USDZ archive reading
- `serde` (optional, `serde` feature) - Serialization support

Domain schemas are gated behind per-module features (`geom`, `lux`, `physics`, `render`, `shade`, `skel`); use `--all-features` when building, testing, or linting.

The project maintains a minimal dependency footprint and uses cargo-deny to prevent license conflicts and vulnerability introduction. Allowed licenses: MIT, Apache-2.0, Zlib, Unicode-3.0.