---
name: luncosim-architecture
description: Review or build a LunCoSim feature that crosses USD, Modelica, Avian, Rust, or Rhai. Use this when adding a reusable component, sensor, actuator, controller, generated Modelica network, USD schema, or runtime projection, and when removing legacy paths, shims, compatibility fallbacks, or custom fields that duplicate an OpenUSD standard.
---

# LunCoSim architecture

Use this skill before changing a reusable engine feature. Keep the authored
system declarative and composable, and make each concern live in its native
representation:

Before calling an architectural capability missing or impossible, use
[**capability-discovery**](../capability-discovery/SKILL.md). Search the relevant
skills and architecture docs, identify the standard or project owner, inspect
registrations and callers, check maintained dependencies, and verify the
production/API surface when relevant. Classify the result as found,
implemented-but-unwired, present on another branch/version, not found in the
searched scope, or externally blocked. Do not create a second owner, fallback,
or compatibility shim because the first search was incomplete.

| Concern | Authoritative owner | Runtime role |
|---|---|---|
| Scene structure, identity, topology, frames, connections, component parameters | USD | Rust projects the composed stage; it does not invent missing topology |
| Continuous equations, state, control laws, filters, physical networks | Modelica | Rumoca compiles and steps the model |
| Rigid-body collision, contacts, forces, torques, joints | Avian through USD-authored physics | Rust exposes the engine's generic mechanics and executes them |
| Mission phases, events, policy, objectives | Rhai or behaviour trees | Task/event orchestration in production; `on_tick` is test-only for sampled verdicts |
| Engine mechanisms, projection, scheduling, hot paths | Rust | Generic implementation; no vehicle- or sensor-name special cases |

For cross-domain execution, read
[`62-deterministic-runtime-and-async-boundaries.md`](../../docs/architecture/62-deterministic-runtime-and-async-boundaries.md).
Keep parsing, source resolution, and immutable preparation off the UI/fixed
schedule when inputs can be captured by revision. Admit results only at an
owner boundary in stable identity order. Keep live-world hooks and physics
inside their deterministic schedule. The required Rhai USD composition policy
decides incomplete initial stage and newly mounted reference closures before
their respective admission and projection boundaries. A Rhai scenario that
depends on Modelica
ports or events declares the participating entity ids in
`simulation_dependencies(me, ctx)`, where `ctx` is the validated scenario
parameter map. The hook returns a map with `modelica_entities: [ids]`,
`entity_reads: [ids]`, `entity_writes: [ids]`,
`query_reads: ["PublicQueryName"]`, and
`required_inputs: [#{ owner, identity }]`. All five arrays are required even
when empty. The owner resolves all declared ids once per
source/parameter revision. `modelica_entities` ids must identify live Modelica
participants and grant both directions of access. Directional read/write ids
may identify any live entity; a Modelica entity in either set also joins the
shared causal barrier. Unresolved ids fail that source revision with a visible
diagnostic.
Every simulation-clock Modelica port access must be covered by the calling
scenario's own plan, including `get`, `port`, and
`query("ReadPorts", #{ api_id, port_names })`;
aggregate barrier membership from USD wiring or another scenario does not
authorize the read or write. The owner enforces this at the port, query,
targeted-command, and event-delivery boundaries.
Generic reflected reads and direct mutations also require the matching
`entity_reads` or `entity_writes` declaration. After a typed simulation
command materializes an entity whose id was not available during plan
preparation, `track_entity_read(id)` or `track_entity_write(id)` adds the
directional access in serialized simulation order before the entity is used.
Tracked Modelica entities join the same barrier when they enter the current
Modelica projection.
Entity-targeted API query providers report their live target ids through
`simulation_entity_reads`; the scripting bridge checks them against the
scenario's read plan before execution. Broad spatial/world queries use an
explicit coarse declaration by public provider name in `query_reads`.
Mounted-scene USD queries without `doc_id` use the active committed Twin
generation. Unknown query providers default to the broad declared scope, and
broad queries cannot run while `simulation_dependencies` is resolving.
Required input keys refer to producer namespaces registered in the generic
`SimulationDependencyStates` resource. The scenario holds its existing
`ScriptPreparation` key until every required input is Ready; it retries only
after an owner-published state revision, and the visible wait reason names the
input. A missing producer is a terminal diagnostic; a producer's Failed state
reports its errors. Omitting the hook declares no Rhai dependencies. The plan
runs before mutable top-level initialization, so derive it from `me`, scenario
parameters, and read-only world queries. Top-level initialization runs in its
own `Initialization` phase after the plan commits. Dependency planning may
resolve identities but cannot access live ports, issue commands, mutate the
world, or emit events. Invalid or unresolved ids fail that source revision
with a diagnostic. Unbarriered port access and Modelica event delivery
fail visibly. Connected Modelica event edges are sampled only while the
simulation clock advances, after `SimTickSet` and before Rhai; this gives an
initially active output its producer tick after scenario readiness. USD
simulation projection uses stable logical stage source, optional instance-root
path, and authored prim path for bounded admission, topology-preparation order,
and dynamic-body promotion; missing or ambiguous identities fault, remain
queued, or stay kinematic.
Physics operations that can accumulate into shared bodies use `PhysicsOrderKey`
from the stable logical stage source, instance root, and authored prim path.
Joint solving, motor warm-start, custom prismatic correction, raycast and jointed tire forces,
and raycast mass-property folds consume stable key order.
The production `multi_rover_stress` Rhai gate selects a profile from typed
scene-test parameters and compares only the requested reference row at each
sample. Rust loads the portable JSON at the `--determinism-reference PATH`
process boundary and exposes typed values through the existing Rhai query
bridge. Rhai owns profile matching and exact comparison (`numeric_tolerance=0`)
for six physics checkpoints, selected Modelica and articulated checkpoints,
and the explicit final physics, Modelica, and articulated state. The test keeps
only checkpoint tick numbers and the first mismatch; it also verifies Rhai
rejects a deliberately altered physics row and does not emit or retain a trace
bundle. The Bash and PowerShell matrices in
`scripts/test-deterministic-physics-profiles.sh` and
`scripts/test-deterministic-physics-profiles.ps1` invoke regular production
`luncosim test` processes and trust their exit status. They leave `--tick-hz`
unset, so the default fixed step is 60 Hz; the runner advances the manual clock
without wall-time pacing. This remains fixture-specific evidence. Do not claim
whole-simulation replay determinism while the remaining reviewed gaps are open.

Reflected commands sent through `ApiCommandEvent` retain whether they came from
an API transport or a Rhai evaluation; generated `CommandOccurred` facts carry
that origin. The workbench uses `PanelCtx::trigger_command` to carry an
explicit local-user `SessionId` across deferred UI dispatch; the port inspector
therefore enters fixed-tick input admission. Direct typed Bevy triggers without
an explicit origin remain unclassified, and the occurrence fact still lacks
command parameters, target, scene generation, effective tick, and per-tick
input order. Rhai scenario origins carry their stable actor id and source
execution sequence.
`SimulateIntentEdge` preserves the complete origin through its typed semantic
edge into the bounded `CausalTrace` query; Rhai records include owner route,
phase, generation, sequence, and actor id. API, application-Rhai, and direct
typed discrete edges receive a committed scene generation, next `SimTick`, and
per-tick sequence, then reach the event bus from the fixed-step owner.
Simulation-clock Rhai edges remain derived behavior and do not receive this
external-input stamp. The queue is bounded to 4,096 pending records and clears
on scene teardown; the trace is not durable replay storage. External held
`SimulateIntent` changes share the bounded queue and per-tick order with edges.
API, direct typed, and actorless Rhai producers provide a stable nonzero
`producer_id`; Twin Rhai retains its route and stable actor identity. Held
acknowledgements, telemetry, `CausalTrace`, and session records retain producer
identity. `drive_from_bindings` captures physical
`ActionState<UserIntent>` into a by-value `PhysicalIntentFrame` semantic
snapshot. Admission requires the local input `SessionId`, target
`GlobalEntityId`, and committed scene generation. The fixed-step owner stamps
the frame with those identities, the current `SimTick`, and a sequence from
the shared `lunco-control-core::SimulationInputOrderAllocator` before
controller translation. The allocator resets on scene teardown so producers
share one per-tick order space. Admitted external semantic payloads wait in
`lunco-core-session::PendingSessionInputs`; core-session clears that queue on
scene teardown. Its fixed-step coordinator validates and captures due records,
then publishes typed commit events in shared sequence order while simulation
time is running; a pause leaves inputs queued for the next running tick. The
controller applies semantic payloads before physical input sampling.
Raw-file runtime spawns also enter this commit boundary; the scene-command
owner checks the current frame and catalog and inserts the reserved root
identity before identity admission. Physical frames remain sampled at their
consuming fixed tick through the same per-tick allocator. Missing facts or
duplicate target/session order keys hold input with a structured runtime
error; ordering does not fall back to Bevy `Entity` bits. An active
`SessionInputStream` captures sorted canonical intent ids, admitted controls,
lifecycle input-hold releases, and raw-file spawn records.
`ControlAuthorityChanged` queues `ReleaseControlInputs` for the next fixed
tick. At its ordered commit, the co-simulation owner clears controller-owned
port holds and the controller clears held semantic intents; authored program
setpoints, endpoint values, and physics state remain intact. The release writes
no stop value. Twin policy owns any stop setpoint and issues explicit named
`SetPorts` writes. Capture retains the ordered lifecycle record when active.
Missing target, generation, tick, or order facts produce a runtime error and
leave existing holds intact. Spawn records retain producer,
correlation, stable scene-root and active-frame identities, original f64 pose,
admission stamp, and reserved root id; acknowledgements expose that stamp.
Document-backed `SpawnEntity` still authors only `ApplyUsdOps` into the Twin
journal. Capture remains bounded and in-memory; baseline manifests, playback,
and other typed commands remain open.
The persistent canonical `WorldGrid` has deterministic content provenance, so
the default active physics frame also has a stable `GlobalEntityId`.

Scenario actor compile submission, completion commit, and hook execution use the
source-owned `GlobalEntityId` component directly; the Update-synchronized API
lookup index is not an ordering source. A local-only host without that component
uses its Bevy entity key only within the current World and has no cross-session
replay identity. Author observable multi-actor ordering checks in Rhai scene
tests; reserve Rust tests for the generic ordering and async commit mechanisms.

For file-backed documents, use `PreparedFileBacked` with the shared
`DocumentRegistry::open_prepared_file` path when source parsing moves to a
worker. The registry remains authoritative for path identity, dirty-document
preservation, and lifecycle events. Owners fence results against the exact
source/document revision before committing them. The default USD Twin scene
uses this path for native USDA parse and persistent-source serialization;
runtime sidecar restore and the browser worker transport remain separate open
boundaries.

Twin teardown releases domain documents and browser preparations before the
replacement is admitted. Reopening keeps logical source identity but receives
a fresh load authority. Resolve authored dependencies through
`lunco-assets-core::asset_path::load_asset_path` with the admitted live origin;
retired origins fail before rebinding, and origin-free requests keep exact
transport authority. Background file snapshots resolve logical source names
and fence publication by scope and mount revision. See
[`61-scene-lifecycle-and-teardown.md`](../../docs/architecture/61-scene-lifecycle-and-teardown.md).

Lifecycle work that changes authoritative scene state participates in
`lunco-core-runtime::SimulationProgress`. Acquire with a typed owner/operation
key, carry that identity through async preparation, and release it only after
the owning terminal result has been committed. Scene load/restart/clear use
`SceneTransitionId`; same-path transitions still have distinct identities.
The coordinator advances the shared scene generation only for the matching
successful transition and emits `SceneTransitionCommitted`. Scenario and other
Twin-scoped cycle owners read that committed generation; do not maintain a
subsystem-local scene counter or arm lifecycle work from a stale completion.
Lifecycle hooks receive a discrete owner context keyed to the exact transition.
For example, `scene.time.select` runs as `Twin/Lifecycle/Preparation` with no
elapsed clock, and its typed result carries the same `SceneTransitionId` through
application. The time owner ignores stale results and terminal edges after a
replacement begins. One-shot policy inspection uses `Application/Repl/Evaluation`.
Keep participant readiness and `PhysicsHolds` in their owners: they control
local/world physics admission, while `SimulationProgress` controls whether the
shared causal tick may advance. `UsdSceneRuntimePlugin` installs this shared
resource when selected and holds active reference spawns on the mounted primary
scene through closure preparation and live ECS projection. Preview and additive
mounts retain diagnostics without pausing or faulting the primary simulation. A
terminal primary reference failure retains its exact hold and publishes a path
diagnostic plus `RuntimeFault` until scene teardown; inactive or removed,
nonfaulted operations release their own keys. Surface the active wait reason
through the existing status bus. Async data that changes authoritative physics,
including a mounted DEM download/build, also holds an exact terrain entity key
through the committed collider/oracle result; on web the hold spans the coarse
preview and full worker result. The DEM bridge and readiness scan run in
`PreUpdate` before `SimulationAdmissionSet`, including while a Twin manifest scan is
pending, so the first eligible fixed tick cannot precede terrain admission.
UI and presentation schedules remain live.
Persistent edits to the mounted primary USD document hold one coalesced
`UsdDocumentProjection` key from the observed registry revision through the
matching ECS projection commit; disposable view-layer edits do not hold world
time when their typed operation suffix is available; an unavailable suffix is
conservatively treated as causal. Change detection admits UI/command edits in
the `SimulationProgressAdmissionSet` after entity indexing and before the
`PreUpdate` simulation-admission boundary, and admits edits issued inside a
fixed Rhai/event pass in that lane in `FixedLast`. The fixed runner completes
the current cycle, then preserves its remaining overstep until the projected
revision is committed. Do not poll
document contents on steady frames or release the hold from a stage-only cursor
before ECS projection completes.
Physics readiness then admits bodies and joints on fixed steps. An active USD Modelica participant remains
readiness-held through its first successful communication point; a compiled,
intentionally paused model is ready without being stepped. Keep that participant
readiness separate from the shared `SimulationProgress` lifecycle gate.
First-compile intent is admitted by `request_modelica_compiles` in the lifecycle
cycle while solver stepping stays in `FixedUpdate`, so compilation can be
requested while virtual simulation time is held. The Modelica execution owner
consumes typed `CompileRequested` intent, validates its pinned document runtime
lifetime, and owns same-owner source gathering and worker dispatch; the UI command only chooses the class and publishes intent.
On native, source-root installation, compile requests, Reset, parameter updates,
and cache-invalidating Step auto-init share one single-owner Rumoca actor FIFO.
Results return asynchronously and commit in submission order; actor requests
and solve preparations use bounded admission. A Step that requires a rebuild
resumes only after the matching session and library generation commit.
Compile results must match both worker session and captured document
generation. A result for an edited source revision is discarded, and an active
model keeps its compile-run intent until a current result commits. The execution
owner reconciles active causal models into `SimulationProgress` before
`SimulationAdmissionSet` and releases each exact entity key only after the matching
compile result has been committed. Intentionally paused and noncausal models do
not hold world time. Their first normal co-simulation step uses the per-step
barrier after activation. The root USD loader composes the available dependency
closure before publishing the stage asset, and scene admission ends after
structural projection. CPU mesh construction stays on the presentation path.
Keep owner-specific clock gates separate; production acceptance must still
prove that the first authoritative fixed tick observes each dependency declared
by the scene and scenario, independent of worker completion order.

For engineering requirements, keep the same split at the numerical boundary:
SysML owns typed intent, units, normative tolerances, and requirement/
verification identity; USD owns realized geometry and standard scene facts;
Modelica/Rumoca owns equations and continuous state; Rhai owns the selected
mechanical relation, orchestration, and evidence; Rust owns only reusable,
hot, type-safe f64 mechanisms. The authored
`assets/scripting/tools/mechanical_relations.rhai` library is the extension
point for CAD-like predicates such as distance, coincidence, parallelism,
under/clearance, mirroring, and symmetry. Do not grow a Rust registry of
relation names or product-specific checks.

Use the existing Rhai standard math surface for ordinary scalar operations and
the native Rust bridge for f64 vector validity, dot/cross, clamped cosine,
angle, and native component access. Keep native values in hot loops; arrays are
only explicit interchange boundaries. Use typed predicates (`f64_only`,
`array_is`, `map_is`, `string_is`, `vec3_is_native`) instead of string-based
runtime type protocols. Numerical settings are explicit per dimension and are
resolved once per report/solve; they do not replace a source-owned SysML
tolerance or become a global epsilon.

For Avian-backed physics, keep one numeric admission contract at
`lunco-physics::avian_backend`. The BigSpace bridge owns lifecycle admission of
f64 poses and collider support geometry before the Avian step, `GridSpatialQuery`
reuses the converted-ray predicate, and USD projection reuses shape/AABB and
leaf structure predicates before ECS insertion. Convex colliders are checked by
their actual support points; composite or non-convex shapes use the conservative
AABB boundary. A failed live invariant raises the
shared scene-scoped runtime fault and gates the remaining nested physics
phases. Do not turn these checks into per-call query fallbacks or lint-only
warnings.

### Rhai task callback contract

Task leaves accept anonymous closures (`|me| ...`) or named script callbacks
(`Fn("name")`, declared `fn name(me)`). `me` is the host entity id; both forms
receive persistent state as the driver-bound `this`. The native task driver owns
progression and state transfer. Rhai map parameters are value/copy-on-write
values: a helper that assigns a state field must return the updated map, and
the lifecycle callback must assign it back to `this`. Do not rely on helper
side effects to persist task state.

## Plugin and crate layering

Use a domain `CorePlugin` for headless-safe state, lifecycle, commands, and
runtime mechanisms, then add a separate UI plugin only for panels and visual
presentation. Do not create a UI plugin merely to host API queries. If a
provider belongs to a core domain but importing the API crate would create a
dependency cycle, put the provider in a small `*-api` adapter crate and have
each API-capable composition root install it explicitly. Keep the data/core
crate independent of transport and presentation layers.

For dynamic providers, keep `lunco-hooks` as the single reflected contract and
invocation owner. `HookInvocation` carries the typed runtime context with the
validated values. `lunco-hooks-plugin-api` owns the stable edition-2024 ABI v2
and typed invocation wire helpers; `lunco-hooks-native` owns `libloading`, unsafe
admission, callback limits, and exact-registration teardown. Enable that path
only from an application composition feature. A Twin must explicitly approve a
provider with a Twin-relative `[[native_plugins]]` manifest entry. Reject rooted,
volume-relative, traversal, and alternate-stream paths; resolve through the asset
owner's canonical containment check before loading. USD and Rhai
source do not load shared libraries. Native code is trusted process code, so
untrusted bundles require deployment-level signature and isolation controls.

A provider may implement only an existing reflected, installable hook and must
return typed data or a validated action plan. It must not mutate ECS/USD or add
a second domain registry. The owner invokes it through `lunco_hooks::invoke`
with a typed `HookInvocation`: cycle-owned callers supply their runtime
context, while discrete boundaries explicitly use an unclassified context.
The owner validates the result and reports a fault or unconfigured state
according to the hook contract. Policy manifests may mark an optional
feature-owned hook with `skip_when_hook_unavailable`; the runtime reports that
capability in `policy_status().unavailable`, while compile and activation
failures remain in `failed`. Put an eventual expensive terrain provider at the
consumed `lunco-terrain-bake` kernel boundary; keep `lunco-terrain-core` projection-free
and leave `lunco-terrain-surface` as the runtime projection owner. See
[`native-hook-providers.md`](../../docs/architecture/native-hook-providers.md).

Keep stateful per-entity Rhai scenarios on their owning Simulation lane. For a
stateless Rhai decision at another cadence, prefer an existing owner-invoked
hook with typed facts and context over adding more lifecycle callbacks to the
scenario runtime. Cross-cycle results return as typed owner actions; a scenario
`this` map never moves between cycles.

Keep the workbench split at the dependency boundary: `lunco-workbench-core`
owns renderer-independent panel/menu/perspective/registration contracts, tab
navigation, source-view commands, scene display state, pending tab-close state,
and the published `WorkbenchSnapshot`; `lunco-workbench-widgets` owns
shell-independent egui controls;
`lunco-workbench` owns `egui_dock`, `bevy_egui`, viewport rendering,
persistence, source editing, and command observers; and
`lunco-workbench-guided-ui` owns the optional Rhai-driven HUD, spotlight,
coach-mark, and guided-recovery surfaces; and
`lunco-workbench-browser` owns the optional Twin/Files panels, browser state,
and built-in filesystem/library sections without depending on the concrete
shell. Rename payloads belong to `lunco-doc-bevy` or `lunco-workspace` according
to the identity they address; the shell retains only picker/execution
observers. Domain UI crates implement contracts from the core crate, read
layout facts from the snapshot, and depend on the concrete shell only when
they use those presentation services. Do not expose or consume the shell's
private `WorkbenchLayout` outside that crate.

Keep application-edge presentation separate from the reusable shell:
lunco-luncosim-presentation owns the final status/environment, terrain-horizon,
USD camera/light, capture, and scene-presentation bridges, while
lunco-luncosim-ui owns window/plugin composition and installs that package at
the boundary. Native updater startup and its rendered surface belong to the
optional `lunco-updater` package, not to the ordinary UI closure. Celestial
body projection and cadence belong to `lunco-celestial-spatial`; ordinary
moving scene objects use standard USD `timeSamples` through
`lunco-usd-bevy-animation`. Do not create a mission-only trajectory component
or clock when composed USD animation expresses the motion.
Celestial state uses the one `lunco-time::CelestialTime` sample, an affine
child of `WorldTime`. It drives ephemerides, body rotation, solar irradiance,
lighting, shadows, geometry queries, and target positions consumed by the
generic EnvironmentProbe direction resolver. `SunState` contains irradiance;
it is not a parallel direction source. A rate up to 100,000× leaves Avian and Modelica at their ordinary
fixed-step cadence. From lunar ground, Earth stays near one sky position
because the Moon is tidally locked; its axial spin still advances the day/night
pattern. BigSpace propagates both the changed Earth grid pose and rotation to
`GlobalTransform`; one cadence gate commits the CelestialTime sample it read.
Static authored-light rays are selected only after the active composed root's
celestial projection has classified its source ownership. A celestial root owns
the finite body target exclusively; any static ray seeded before classification
must be withdrawn before probe resolution.
The celestial hierarchy must not wait on interactive input bindings: invalid
bindings may withhold the optional observer camera, but must not suppress the
finite targets consumed by physics and Modelica.

### Runtime scopes, cycles, and publication boundaries

Use the shared `lunco_runtime_context::RuntimeScope` and
`lunco_core::RuntimeCycleSet` vocabulary when
placing a cross-cutting system. `Core`, `Application`, and `Twin` describe
ownership; `Lifecycle`, `Simulation`, `Interaction`, `Command`, `Repl`, `Telemetry`,
`Ui`, `Presentation`, and `Visualization` describe cadence. These are schedule labels and typed route
metadata, not a new global event bus. Twin-owned resources and completions must
carry their mount generation and be retired at Twin teardown.

Cycle labels do not create independent clocks, CPU isolation, or execution
cadence. Rust plugin composition decides which capabilities a host installs;
Bevy's owning schedules provide the actual execution boundary. A typed
`RuntimeExecutionContext` carries the current owner route, cycle, phase, clock
sample, logical sequence, and optional event producer stamp into synchronous
Rhai calls. Scenario hooks and one-shot REPL/tool calls use their owning
contexts: `RunRhai` runs as `Application/Repl`, while typed UI tool callbacks
run as `Application/Ui` after picking in `PreUpdate` and before fixed
simulation. `execution_context()` exposes a read-only Rhai map. Generic hook
calls carry `HookInvocation`; nested `invoke_hook` forwards its active context,
and an isolated Rhai hook reads it from the immutable `runtime_context` map.
The shared hook registry rejects a clock that does not belong to its cycle,
missing or invalid clock samples, and non-zero Core/Application generations
before invoking either Rhai or native policy.
`twin.lifecycle` uses the mounted Twin's `TwinId` as its `Twin/Lifecycle`
generation, with explicit `Start`, `Event`, or `Stop` phase and no elapsed
clock; `policy_status().lifecycle.runtime_context` exposes the exact stamp.
Rust call sites without a classified owner use the explicit unclassified hook
entry point. The physics escape policy is a scheduled core simulation hook and
receives its `Behavior` context from `SimTick` and `Time<Fixed>`; its authored
policy rejects off-cycle calls. The scheduled `readiness.action` policy uses
`Core/Simulation/Behavior` with `Time<Fixed>` and the latest `SimTick`, and its
authored policy rejects calls from other cycles. `sim_tick()`,
`dt()`, and `elapsed_seconds()` reject calls outside the simulation cycle as
invocation errors, while missing mandatory simulation clock resources remain
runtime faults. An unclassified invocation has no route; Rhai exposes its
scope, cycle, and generation as unit instead of inventing an owner. Add a
separate schedule driver only when a cycle needs independent cadence or
overload semantics, and keep expensive calculations off
the UI/physics-critical thread.

Native immutable preparation uses `lunco_core_runtime::AsyncWorkAdmission`;
do not add another per-crate priority queue. Each request carries its scope
generation, stable owner identity, source revision, and operation id. Priority
selects which queued job starts; the owner still validates and commits its
typed result at its own boundary. WebAssembly's Bevy async-compute pool runs
cooperatively on the browser main thread, so expensive web work needs an
explicit worker transport rather than this native dispatcher.
Native terrain cover analysis and streamed tile-mesh bakes share this admission.
Tile bakes use `Interactive` priority and a stable owner-selected nearest-first
rank; the terrain owner fences completions by operation key and publishes meshes
in stable coordinate order under its frame budget. Surface changes and owner
removal withdraw queued work, and already-running stale results are discarded.
Keep the browser tile-cache path explicit until Web Worker transport is
available.
Serialize each Modelica step's input assignments in variable-name order;
never let hash-map iteration choose a worker command's observable order.
File-backed Rhai source assets are parsed and const-folded by the asynchronous
asset loader, which publishes their canonical id, exact text, AST, and literal
import dependencies together. Activation owners commit the complete loaded
dependency graph before binding or starting a source; Bevy can report graph
readiness before its `Added` messages are consumed. Startup/Twin tools and the
prelude reuse that AST; scenario workers reuse it only when the source bytes
match, and parse inline roots or uncommitted sources inside shared admission.
Do not compile loaded tool or prelude source synchronously during registration.
The asset publisher retains each loaded `AssetId`'s canonical URI and retires it
on `AssetEvent::Removed`, because `AssetServer::get_path` may fail after the
last handle is released. Ignore the earlier `Unused` edge, and prevent a stale
asset removal from deleting a replacement source with the same URI.
Twin SysML analysis follows the same boundary: `twin.lifecycle` selects the
checked manifest source set, `PrepareTwinSysmlAnalysis` loads source assets and
prepares one immutable snapshot through shared admission, and runtime
`twin://` queries read only the current Twin-id/root/operation result. Pending,
failed, and unprepared snapshots remain visible. This read-only active-Twin work
uses `Interactive` priority and never holds `SimulationProgress`; a scenario
that requires SysML facts declares the typed owner key in its dependency plan.
Open SysML documents use the same bounded `Interactive` admission. Lifecycle
events capture immutable source/origin facts, and `SysmlDocumentAnalyses`
commits only for the exact current document generation and origin URI.
`InspectSysmlDocument` returns `analysis_state` as `pending`, `ready`, or
`failed`; pending diagnostics and error fields are null. Rhai editor verification
must return a retryable pending result and must not call it clean. These editor
analyses do not hold simulation progress. Browser worker transport is explicit
and remains unsupported by this native dispatcher. The Twin analysis producer
registers `sysml.twin-analysis` and publishes each mounted Twin's state under
its exact name. A scenario that needs those facts lists
`#{ owner: "sysml.twin-analysis", identity: twin_name }` in
`required_inputs`; only that scenario's activation hold waits for the async
analysis. The analysis worker itself remains read-only and never acquires a
world-time hold.
Telemetry samples are captured with the fixed tick, then delivered through the
plugin-owned bounded `Telemetry` cycle. Never run subscription, retention, or
logging observers inline with fixed physics; report queue loss through the
telemetry status query and keep simulation progress independent.
The GUI installs `TerrainSurfaceVisualizationPlugin`; server and scene-test
compositions keep terrain physics/query support but omit camera-driven LOD,
visual-map baking, and overlays entirely. A system hidden behind a server-mode
`run_if` still exists in that schedule and is not equivalent to omitting it.
Application builders install the selected capabilities automatically; authors
should not assemble Bevy schedules by hand. Rhai currently declares peer-role
selection through `@peer host|client|both`, with simulation as its supported
timing. An unknown execution target, unsupported timing, or unknown metadata
directive disables only that scenario and publishes one document error for its
source generation instead of breaking the host, defaulting to host, or guessing
a clock. Cycle and clock selection come
from the Rust owner, not a script directive. A callback error remains visible
and local to its owner; required authoritative hooks hold/fault their owner.
Never panic or silently report success for a failed hook.

The initial camera policy receives `Application/Presentation/Preparation` with
the `Time<Real>` presentation clock. Its `RuntimeCycleSet::Presentation` labels
are ordering metadata inside `Update`; they do not give UI or visual LOD an
independent cadence. The runtime UI recording selector receives
`Application/Ui/Preparation` with the Application clock in its `PostUpdate`
owner chain; it runs only when its revisions change, not as a separate cadence.
The authored runtime-surface visibility/property policies receive typed
`Application/Ui/Preparation` context from exposure publication; camera-status
properties receive `Application/Presentation/Initialization` or
`Application/Presentation/Event` from the camera-status publisher. Add a
policy guard and verify both the scheduled owner context and an off-cycle call
when assigning context to another hook. The `route_lifecycle` production gate
checks `ReadExposures` for an authored program-browser surface after metadata
admission.

The rendering-quality catalog receives
`Application/Presentation/Initialization` on startup and
`Application/Presentation/Preparation` for stale-policy refreshes, both using
`Time<Real>`. The render shadow-warning policy receives Presentation
Preparation context from its `PostUpdate` owner when its configuration changes.
The Presentation cycle set remains ordering metadata and does not add a
separate cadence.

The event-driven `usd.component_refresh` owner supplies
`Twin/Lifecycle/Preparation` with the active or committed generation for a
mounted-Twin document, and `Application/Lifecycle/Preparation` for a preview-
only document. The owner requires a generation for a mounted Twin; a missing
generation rejects that policy-controlled refresh visibly.

Cycle labels identify ordering and ownership; they do not imply that every owner
already publishes timing and queue metrics. Expose bounded aggregates at task
boundaries where measurements are needed, then use them to target a Tracy
capture and measure frame responsiveness in a separate unprofiled run. Do not
feed a per-frame diagnostics firehose through the simulation telemetry sampler.
`CosimStatus` exposes per-session Modelica solver-step duration, dispatch-to-
response latency, and the native worker's pending-task count at step start.
Pass `include_values: false` for a bounded fleet view without input/output maps
or verbose model/error details; status, counts, and timing remain available.
Rhai profilers can pass `include_entities: false` to read aggregate worker
metrics without constructing a string-bearing row for every participant.
Response latency includes queueing, transport, and owner response handling; it
is not a pure queue-wait measurement. The browser worker does not expose its
internal queue depth.

Keep telemetry's two lanes distinct: authoritative events used by Rhai retain
their producer tick and deterministic delivery order; continuous samples are
bounded observations of committed state. The fixed sample boundary captures
due channels into a small typed record. A cached channel plan still requires a
fixed-path walk and live port reads; its samples then go through a bounded
post-simulation cycle. Logging, subscriber fan-out, formatting, serialization,
persistence, and UI plot decimation run outside the physics transaction.
Reducing fixed-path sampling work and measuring its effect on throughput remain
open owner-level work, not a completed guarantee.

Commands and one-shot Rhai/REPL evaluations have independent application
clocks. Record command cadence from the shared `CommandOccurred` publication
and REPL cadence from actual `drain_world_scripts` evaluation. Both use the
wall clock, never simulation time; a queued script is not counted as evaluated
until the exclusive REPL owner runs it. Publish their sequence and timing
through `ApplicationCadence`/`application-cadence` so consumers do not add
their own timers or infer cadence from render frames.

World-bound one-shot Rhai requests remain serial because their verbs read and
mutate the live World. The REPL owns a bounded FIFO (64 pending requests by
default), evaluates one request per `Update`, and applies a 100,000-operation
ceiling per invocation. Queue overflow is a terminal command error. Keep
scenario and hook execution in their owning clocks; do not parallelize callbacks
that still use live-world access.

Publish each fact through its one domain boundary before adding a consumer:
scalar co-simulation endpoints use `PortRegistry`; presentation-ready values
use `EngineExposures`; lifecycle occurrences use typed events or revisioned
resources. Status bars, authored HUDs, telemetry adapters, API readers, and
recorders consume those publications. They must not independently scan engine
diagnostics, query a physics owner, or introduce a widget-specific registry.
The fixed-step and rollback co-simulation paths share one propagation function
and compiled cache; transform propagation remains a separate spatial owner.
Rollback is an instantaneous replay cycle over recorded inputs, so systems in
`RollbackReplay` must not be gated by the live virtual clock's paused/running
condition. Keep its actuation ordering aligned with the fixed path and test the
real replay schedule while `Time<Virtual>` is paused. This body prediction
replay does not reconstruct whole-session commands, Rhai state, or Modelica
state; do not claim whole-simulation replay determinism from it.

For a `ShaderLook` with `vertex_shader`, treat the fragment and vertex sources as
one linked material contract: both stages read the same `@binding(0)` uniform
block, so their `Material` fields, order, and WGSL types must agree. The
fragment schema is the packed layout; do not invent a second vertex schema or
silently select a replacement shader. Validate the requested stage positively:
`sourceAsset` needs `@fragment` and `vertex_shader` needs `@vertex`; a combined
WGSL module is valid for either role. A missing or invalid stage is a structured
runtime diagnostic and an unbound material. Rust must not install a
`StandardMaterial`, neutral shader, or another guessed source as recovery. If a
scenario needs recovery, make it an explicit Rhai policy that can be replaced
or disabled without rebuilding the renderer. Add a cross-file ABI test when
maintaining a multi-stage shader pair. See
[`shader-layers-and-params.md`](../../docs/architecture/shader-layers-and-params.md).

Treat an illumination-bearing grayscale orthophoto as measured imagery, not
intrinsic reflectance. The native `lunco-assets-processing` `kind = "albedo"` pipeline
removes its low-frequency illumination field, anchors local detail at a
neutral material base, and sRGB-encodes the resulting linear colour for the
8-bit PNG contract. The texture loader decodes that material back to linear;
terrain shaders use it directly and have no orthophoto compensation function.
`kind = "map"` remains an analysis/display contrast map and must not be bound
directly as `inputs:albedo_map`. When `weight_albedo` is authored, it owns
terrain colour variation; scale procedural dust/mottle by `1 - weight_albedo`,
while keeping relief normals, roughness, ambient occlusion, and photometry
independent. Heavy raster math stays in Rust; Rhai assembly policy selects and
authors the standard USD material inputs.
The packed surface map's G channel is ambient occlusion: route it through the
shared `terrain_surface_occlusion` helper into Bevy's
`PbrInput.diffuse_occlusion`, never into base albedo. AO is indirect-light
visibility; multiplying it into albedo creates broad false colour patches and
darkens direct sunlight.
The render-side `ShaderLook` binder owns event-driven preparation of filterable
authored RGBA8 maps: it deduplicates off-thread mip generation by image asset
version,
filters colour in linear light, averages scalar maps linearly, renormalizes
normal vectors, and then enables trilinear/anisotropic sampling. Do not skip a
zero-weight map at load time: authored weights are live inputs, while mip
preparation is the separate renderer-owned image lifecycle.

Project-owned persistence policy belongs to the active Twin manifest's generic
settings boundary. A domain may define one namespaced scalar key and expose it
through the existing `SetTwinSetting` path; it must not add a global settings
section or a second cache reader/writer for the same artifact.

The local avatar is a runtime kinematic camera embodiment. Its collision
movement uses Avian's existing `MoveAndSlide` query against standard
`UsdPhysics` colliders in `ActivePhysicsFrame`; it does not need a second USD
body schema or a separate collision representation. An explicit Twin setting
may select a documented unsafe policy, but the movement owner reads that
setting directly and remains safe when the setting is omitted, malformed, or
the Twin closes.

The shared semantic input contract lives in `lunco-control-core`, while the
persisted device-to-intent map lives in `lunco-input-core`, and is not
avatar-only: the workbench
owns one app-level local intent surface for editor actions when an isolated
preview has no avatar. Shared actions such as `CancelIntent` read that surface
through the same `InputBindingsSettings` map, while avatar control continues to
use its own surface; neither path may introduce a raw-key or duplicate binding.
`InputBindingsSettings::default()` is a valid neutral override: it has no key or
pointer overrides and uses the schema-defined `Right` look button until the
application overlays its authored defaults. The authored document remains the
source of product bindings, and persisted explicit values take precedence. An
explicit invalid runtime value must be rejected, diagnosed in the status bar, and
clear every live semantic input map so a stale map cannot continue controlling the
simulation. The Workbench host remains alive with input disabled until settings are
corrected. Invalid persisted sections are removed with a settings warning before
the authored defaults are installed.

## Source-backed program attachment

`AttachProgram` is the one authoring boundary for binding a `.mo`, `.py`,
`.rhai`, or behaviour-tree source to an existing USD prim. Rust owns the
generic command and lowers a complete `ProgramAttachSpec` into one journalled
USD change set. The spec carries the source asset, explicit scalar inputs and
outputs, defaults, native USD connections, and the explicit `realtimeSafe`
promise.

The Models palette, Rhai `assembly_edit::attach_program(...)`, HTTP callers, and the Assembly
editor all use this command. None may insert an ECS marker or maintain a
second program registry. An empty port contract is a valid source-only
attachment, but it is not a running scalar cosim participant; the author must
declare the interface before wiring or stepping it.

### Generic authoring evidence

Authoring review is a Rhai policy over generic typed substrate. The shipped
`authoring_inspection` library owns candidate comparison, diagnostic grouping,
diagnostic navigation, unified inspection records, and inspection-mode policy;
it does not know about rovers, landers, or other product nouns. Rust only
provides the reusable mechanisms: document-scoped `QueryUsdPrim` and
`InspectUsdDocument` reads, exact preview selection/framing, generic
`SetDiagnosticLayers`, and settings-backed view-only camera presets.

Keep the ownership split explicit:

- USD remains authoritative for identity, topology, standard visual/collision
  facts, joints, frames, materials, connections, and provenance.
- Rhai chooses the affected paths, groups findings, decides which diagnostic
  layers to show, and composes the evidence record for a human or AI review.
- The Editor owns selection and preview leases; `FrameUsdPreviewSelection`
  frames one exact composed path and must not fall back to a whole-stage frame
  when the requested path is unavailable.
- Shared settings own persisted view-only camera presets. Presets never become
  USD camera prims, journal entries, or another scene graph.

Use exact `DocumentId`, `UsdPreviewId`, `UsdPreviewViewId`, edit target, and
projection generation at every boundary. A document-local Editor fork may be
queried through its composed document stage even when it has no mounted Twin
projection; a stale mapped Twin document must still fail visibly. Missing
collision or provenance is an explicit diagnostic record, not a fabricated
default. Keep positive and negative contracts in the production Rhai scene
gate; do not duplicate these observable assertions in Rust unit tests.

Document-private viewport mounts are recorded separately from workspace/preview
leases in `DocBackedTwinScenes`. Release them at `DocumentClosed`, or when the
final preview lease ends with no workspace projection owner. The projection
synchronizer must not consume that closure bookkeeping. Catalog mount-set
changes also prune private authorities before replacement scans.

### Shared asset catalog discovery

Asset enumeration belongs to `lunco_assets_runtime::discovery` and runs through the
shared asynchronous catalog listing owned by `lunco-scene-catalog`. USD,
WGSL, Modelica, and Python projections are published from one root snapshot;
they must not add a second filesystem walk or a UI-thread scan. A new
manifest/Twin snapshot advances the listing generation, reopens the USD read
set, and drops older metadata completions. This keeps a Twin opened during an
initial scan complete without allowing stale work to populate its catalog.
`TwinClosed` retires closed-mount metadata, spawn and shader choices before
replacement. It invalidates pending listing/metadata generations while retaining
engine and surviving-mount entries; late completions cannot republish a retired
source.

Asset provisioning follows the same dependency split: `lunco-assets-datasets`
owns declarations and lifecycle state, `lunco-assets-transport` owns native
HTTP retry/resume, `lunco-assets-download` owns verification/extraction and
atomic installation, and `lunco-assets-processing` owns native decode and
baking. `lunco-assets` is only the explicit Bevy worker/CLI composition root.
Its `ProcessorRegistry` selects heavy Rust implementations by authored
`ProcessConfig.kind`; Rhai owns dataset selection and sequencing, with extra
processor values carried in `process.parameters`. Do not grow a central Rust
match or make ordinary runtime readers depend on this provisioning stack.

For derived 3D annotations such as a waypoint route, keep one reusable
presentation tool between authored/runtime facts and presentation consumers.
Resolve authored identities through the authoritative binding map and write the
derived USD view to `@view@` only when the route changes or the view is first
materialized. Keep durable route edits in `@runtime@`, separately from the
source scene. Use standard USD geometry and the existing renderer for depth and
occlusion; do not create a Twin-specific ribbon prim or a per-frame document
edit. A reusable route may be a separate composed USD plan; the tool follows
the selected program's canonical parent scope rather than assuming `/Route`.
The disposable view must use the typed transient USD projection command, so
it is generation-checked and OpenUSD-backed without entering authored undo,
save, or Twin journal history. Marker-root placement, annotation geometry,
labels, and look remain separate owners. Stable frames must do no route
parsing, binding lookup, mesh generation, or marker writes; camera-dependent
label projection is the only remaining per-frame presentation work.

Terrain strokes use producer-owned segment IDs and the mutable
`lunco-terrain-surface` index. Ordinary edits publish dirty texels through
`lunco-materials::float_texture` and the renderer's partial-upload adapter;
capacity growth follows the shared image descriptor and material refresh path.
See `docs/architecture/50-usd-driven-visuals.md` for the ownership and lifecycle
contract.

The same boundary applies across document domains: user Rhai/Modelica edits
use their `DocumentHost` operation path, while file-backed, USD-embedded, and
generated source refreshes use the shared `FileBacked::reload_base` contract.
External refreshes advance the live generation and invalidate consumers but do
not create a second undo/journal entry. Never replace a host to hot-swap source;
that discards history and breaks the canonical document identity.

Failure-path acceptance must use an owner-local, transient fixture or typed test
command. For example, the Scenarios menu may inject its unavailable presentation
state without removing or replacing `TwinRoots`, the active Twin, or scene data;
the default production path remains unchanged and the detailed cause still flows
through the shared `StatusBus`.

Repeated presentation solvers must also be value-idempotent: compare derived
`Transform`/`CellCoord` values before assignment. Bevy marks mutable component
access as changed even when the value is equal, and BigSpace consumes those
signals for dirty-subtree propagation. Guarding an equal write at the producer
is part of the ownership contract; it is not permission to hide a real dirty
input or to add an alternate propagation path.

Use the same owner-local revision/cursor shape for other stable projections:
the Modelica document registry wakes engine sync, telemetry producers pace by
their authoritative model/fixed time and capture a live `GlobalEntityId` in the
render-free `SignalRegistry` before a source can become archived, behavior target paths are cached per
entity and invalidated by authored XML or active-frame ancestry, terrain
curvature reacts to its input components, and globe LOD caches pure selection
until camera/LOD/handoff/residency inputs change. These cursors suppress work;
they do not become a second data source or a compatibility fallback.

Apply this contract to all camera pose owners, including shared interaction
easing, mounted USD followers, cinematic path followers, and the persistent
camera origin. Camera selection/mode policy stays in the application; BigSpace
owns only precision representation and derived transform propagation.

Celestial body frames, the sky-time readout, and celestial geometry queries use
`CelestialTime`. Terrain, stations, and links remain attached to their physical
body frames, which follow that same sample. Unbound USD animation uses the
interpolated physical-time sample. Keep each station and marker on the one
body-fixed grid; do not add a parallel presentation grid or marker copy.

Scenario telemetry is collected only while `ScenarioExecutionGate` is open.
That gate waits for all initial scene readiness holds because scenarios may
reference entities outside their owner subtree. After admission, entity holds
idle only scenarios inside the held owner subtree. Fixed-step delivery after
`SimTickSet` releases only events stamped before the current tick; paused
`Update` delivers discrete events without advancing that tick. A new scenario
reads current owner state in `on_start` rather than replaying events from before
its lifecycle. Clear the outgoing scene's pending batch when a scene transition
closes the gate.

Workbench perspectives publish scene visibility as layout intent. A perspective
that uses the full window as its 3D presentation must opt into
`Perspective::scene_visible_when_docked()` so opening a transient side or
bottom panel does not deactivate the selected scene camera or paint the themed
backdrop over the whole frame. Central editor perspectives remain hidden unless
their slot intent includes `ViewportPanel`; the Workbench still never writes
`Camera::is_active`, and `lunco-usd-bevy` remains the sole camera reconciler.

### Temporary diagnostic visuals

Temporary camera/collider/dynamics diagnostics are runtime presentation, not
USD facts. Reuse the existing `Gizmos` systems and one Twin-scoped
`DiagnosticVisualLease` store: Rhai/API/UI selects an explicit target and
policy, while Rust resolves `SceneViewport`, `StageView`, Avian collider
realization, BigSpace render poses, bounded snapshots, and lifecycle cleanup.
Do not add a diagnostic USD schema, temporary physics entity, per-frame USD
edit, second camera selector, or global name-only registry. Handles must carry
the Twin/root/mount generation and become stale on `SceneTeardown`, reload,
target deletion, or `TwinClosed`; missing targets and unsupported shapes are
visible errors. The draw pass reads finalized render transforms and never writes
physics or scene state. Existing separate debug toggles must converge on this
lease boundary when the feature is implemented rather than gaining another
toggle API. See
[`docs/architecture/temporary-diagnostic-visuals.md`](../../docs/architecture/temporary-diagnostic-visuals.md).

### Assembly document snapshots

Assembly editing starts from the existing document system. Use
`DocumentRegistry::fork` and the domain's `ForkableDocument` implementation to
make an untitled document with a fresh identity; do not create a second scene
model or copy a registry. The document implementation copies authored layers
and invalidates private derived state, while `DocumentHost` copies undo/redo
history by value. The registry attaches a recorder for the new id to the same
Twin journal. Save-As is the first path binding. A derived cache must be
document-owned and keyed by all authoritative layer revisions; full USD
composition and dependency resolution stay with `lunco-usd-compose` and its
existing resolver path.

Native Assembly Editor view-models are keyed by the existing `UsdPreviewId`
session. Derive one prim tree, connection canvas, Inspector subview, and
authored USD subview per open session, then paint the session selected by the
focused `UsdPreviewViewId`. Views share the projected stage but own their
camera/render target and never become document identity. Hidden view cameras
remain inactive, and visible targets are bounded by `UsdPreviewRenderBudget`
(2048 px per axis, 4,194,304 pixels per view, and 8,388,608 visible pixels per
frame by default). The shared ECS
selection is only the focused-session projection; keep selection and drilled
targets in editor-owned session state so focus changes cannot apply a command
to a same-named prim in another document. Always carry the session's explicit
`DocumentId`, `LayerId`, and projection generation into a typed USD command.
The editor's path-selection command is `SelectUsdPrim`: it requires the
focused `UsdPreviewId` and resolves through that lease's stage and preview-root
hierarchy.

When the work is an agent-driven human asset edit, use the
[interactive Assembly Editor runbook](../edit-usd-assembly/SKILL.md). The
authoring session is headful and remains visible to the user; every coherent
change is applied through the existing typed/journal path, inspected through
the focused preview and a screenshot, and reviewed with the user before the
next material change or save. This is an operating mode over the existing
ownership model, not a new assembly API.

The render-side camera binder also owns Bevy's clustered-light policy.
Use Bevy's `ClusterConfig::Single` for automatic cameras while the ECS topology
has no point lights, spot lights, light probes, or clustered decals, and follow
those component lifecycle events back to Bevy's normal configuration when one
appears. The camera reconciler must also wait for a positive computed viewport
and positive `Clusters` dimensions before activating a new window camera,
because GPU extraction receives active cameras before the first cluster
assignment. Preserve an explicit `ClusterConfig`; do not add a scene-name
check, per-frame light scan, or alternate lighting implementation.

Light shadow intent follows the same standard-schema boundary: read
`UsdLuxShadowAPI.inputs:shadow:enable` from the composed stage for every light.
Application possession or graphics settings must not overwrite that authored
intent; renderer-owned resource budgets publish facts and let the authored Rhai
render policy report unmet limits, never suppressing authored casters at the
admission boundary.

Scene-root `UsdPrimPath` values may be empty until the stage is parsed. Resolve
that sentinel through the shared USD `defaultPrim` resolver before any domain
projector reads the path; visual and celestial projection must not each invent
their own deferred-path handling or permanently mark an unresolved root.

## Start with the standard-schema audit

Before creating `LunCo*API` or a `lunco:*` property:

1. Inspect the vendored OpenUSD schemas in `crates/lunco-usd-document/schema/core/` and
   the maintained USD/PhysX schema that actually owns the concept.
2. Use the standard field when it exists: `UsdGeom` for transforms, geometry,
   cameras, visibility and purpose; `UsdShade` for connectable graphs and
   materials; `UsdLux` for lights and shadows; `UsdPhysics` for bodies, mass,
   collision, joints, limits and drives; USD metadata and `assetInfo` for
   descriptions and asset identity; USD connections for graph edges.
3. Add a LunCo schema only for semantics that have no standard owner, such as
   mission/celestial meaning, engine program allocation, a LunCo-specific
   sensor configuration, terrain generation policy, or control-session
   ownership. Keep that schema narrow and do not duplicate standard fields.
4. If a custom field overlaps a standard field, migrate every reader and asset
   to the standard spelling in one change, delete the superseded field and branch, and
   regenerate the schema artifacts. Do not read both spellings.

The mapping and the current keep/remove decisions are recorded in
[`references/usd-standard-map.md`](references/usd-standard-map.md). The
authoritative engine architecture is
[`docs/architecture/clean-architecture-and-usd-standards.md`](../../docs/architecture/clean-architecture-and-usd-standards.md).

## Classify a gap before changing the engine

When a report says a capability is missing, verify it against the target
checkout before accepting the claim. Search the shipped asset library, reusable
Modelica packages, composing scenes, and authored tests; read the closest
production exemplar and its consumer. Classify the result as:

```text
engine substrate | reusable asset | mission assembly | external dependency | evidence gap
```

An absent mission asset is not an absent engine capability. A source parse or
documented feature is not runtime evidence. Record the exact source path and
the strongest observed state separately: parsed, composed, contract-ready,
solver-running, numerical behavior, or visual behavior. Only an infrastructure
gap that survives an authored fixture justifies a Rust design.

## Build a reusable component

1. Put reusable geometry, physics, parameters, and Modelica source under
   `assets/`; keep scene-specific opinions in the composing scene layer.
2. Give a component one clear default prim and `kind = "component"`; give a
   composed vehicle `kind = "assembly"`. Use `doc`, `displayName`, `assetInfo`,
   `UsdGeom`, `UsdPhysics`, `UsdShade`, and `UsdLux` before adding namespaced
   duplicates.
3. Represent real attachment with a USD physics joint and its authored frames.
   Hierarchy is namespace, not attachment. A mounted rigid body without a joint
   is a free body.
4. Encapsulate each actuator or sensor as a reusable asset. Instantiate and
   connect it in USD. The lander model must not know that a component is called
   an RCS engine, reaction wheel, or altimeter; it consumes local-frame ports.
5. Let Avian apply force or torque at the authored actuator frame. Do not add a
   special Rust emitter for one actuator family, convert to world coordinates in
   Modelica, or maintain a parallel actuator registry.
6. Expose tunable physical values as typed USD-authored parameters or Modelica
   parameters; do not hide them in Rust, Rhai, or a renderer. Named constants
   are appropriate for policy-owned presentation geometry, spacing, extents,
   and typography when they are clearly separated from physical parameters.

## Build a sensor and controller

1. Expose raw built-in Avian observations through generic ports. Rust may know
   that a ray hit happened and publish distance, validity, normal, point, and
   relative velocity; it must not decide that the observation is an
   “altimeter” or “landing sensor”.
2. Mount the sensor and author its connections in USD. Keep sensor placement,
   frames, collision filters, and parameters in the USD asset.
3. Convert raw observations into useful navigation signals in Modelica. Put
   filtering, derivatives, frame conversion, attitude reference, PID, thrust
   mixing, fuel, and actuator dynamics in Modelica. Modelica uses local frames;
   it does not receive a hidden world-coordinate special case.
4. Treat the parsed Modelica contract as authoritative. A compile-time
   `parameter` is not a runtime input. USD defaults for parameters go to the
   parameter set; only actual Modelica `input` variables enter runtime wiring.
   A missing required port is an authoring error with a diagnostic, not a
   fallback to an alternate name or a fabricated zero.
5. Wire the model's outputs to generic body/actuator ports through native USD
   connections. Use the same path for autopilot, API, and scenario writes.
6. Use possession and an authored authority signal for manual handoff. Do not
   create a second `manual` flag or bypass the control surface.

## Generated Modelica networks

Generated models are a first-class reusable composition boundary:

### Root and island rule

One `CollectionAPI:components` network root produces one generated Modelica
participant with one public boundary. The synthesizer may partition its graph
into several generated Modelica units, but those units remain inside the same
root and are not additional ECS participants.

After validating a generated source, publish it with its parsed interface and
link it to the normal Modelica document. Lifecycle compile admission dispatches
the compile once that document is current, using the same generation/session
fences as authored models. USD projection must not send a direct worker compile.

Scene lifecycle projection, stable entity identity assignment, and API/path
index publication run in that order in `PreUpdate`, before
`SimulationAdmissionSet`. `ClockProjectionSet` gates the following virtual
delta sample, and the fixed runner checks admission before each full tick. Do
not add a separate startup identity pass or readiness
poll; a projected reference must resolve by path on the first resumed tick.

Keep acausal conservation connectors (`Pin`, `HeatPort`, `FluidPort`, `Flange`,
and equivalent domain connectors) inside the root whose solver owns their
algebraic equations. A typed scalar USD connection between two roots is causal
and may have a macro-step or one-step delay. It is not an acausal `connect()`.

USD connection projection caches immutable endpoint facts by composed-stage
generation and runtime-instance identity. Reconciliation retains unchanged
`SimConnection` entities and bindings; edited edges use the normal add/remove
lifecycle. Endpoint lifecycle observers, USD edits, and authority changes feed
the shared `UsdWiringDirty` latch, so stable updates do not scan endpoint
populations for `Added<T>` filters. Domain source-class completion invalidates
only roots that use that source. A root is not synthesized until every
referenced member class has a terminal verdict, avoiding repeated graph
extraction across asynchronous asset arrivals. Scene teardown clears the
scene-owned reverse index, pending candidates, and in-flight synthesis tasks;
resolved member-class facts remain asset-owned and reusable. Cosim source discovery and Python
readiness share one lifecycle-coalesced pending-prim set rather than probing
all USD prims for unprocessed markers on stable updates.

Synthesis hooks run with the owning `Twin/Lifecycle/Preparation` context and
active-or-committed scene generation, without an elapsed clock. Capture that
typed context before async dispatch and pass it unchanged to the hook; the
synchronous live-projection path uses the same contract. Fence async results
by that Twin generation and the owning USD generation or prepared plan. Keep
the policy's context check in Rhai and cover off-cycle rejection plus
generated-source publication in a production scene test.

If zero-delay bidirectional coupling is required, move the coupled components
into one generated Modelica root and solve the combined DAE. Do not add a Rhai
polling loop, duplicate state, or hidden cross-root fallback. Automatic island
fusion is an optional future mechanism; correct network authoring is the
current default.

- USD owns the component graph, instances, port names, parameters, and
  connections.
- The registered generator emits a normal, inspectable Modelica model with
  stable component names, policy-owned unit instance names, and explicit boundary inputs/outputs; runtime-generated
  documents are read-only projections of the authored USD + Rhai policy.
- The generator is selected by an open domain descriptor/registry, not by a
  Rust `if` for “electrical”, “hydraulics”, or one vehicle.
- Acausal equations and physical conservation stay inside Modelica. Causal
  cross-domain signals cross the USD boundary as typed ports.
- Compose reusable Modelica classes through USD before introducing a vehicle- or
  mission-specific `.mo` wrapper. Add new equations only when the maintained
  package and its public contract cannot express the requirement.
- Render the generated Modelica icons and connection graph from the same model
  source; do not create a second visual-only network.
- Make the generated browser entry useful on first click: a single-unit
  network opens its unit class so the member graph is visible, while a
  multi-unit network opens the root wrapper. Keep both classes in the ordinary
  Modelica source/drill-in hierarchy; this is a navigation choice, not a second
  generated graph.
- Keep generated visual synthesis in the selected Rhai policy: standard root /
  unit `Icon` and `Diagram` annotations, policy-owned placements, and any
  domain-specific presentation belong there. Rust may provide generic source
  loading, class resolution, and typed projection metadata, but must not encode
  a domain poster or duplicate the policy's graph.
- For a power-network policy, make common-bus semantics visible with standard
  Modelica `Line` waypoints and a policy-owned diagram rail. Use adaptive,
  extent-aware placement for repeated members. Derive the visual hub from
  graph incidence, using the typed `LunCoModelicaTopologyAPI` `storage` role
  only to break equal-incidence ties; place `source` and `load` roles on
  opposite deterministic banks and pack `neutral` members onto the shorter
  bank. Do not branch on component class names or let a fixed demo layout
  imply a direct source-to-load wire when the composed graph has many members.
  These roles are presentation metadata, not Modelica solver direction:
  acausal flow can reverse at runtime. Keep source/load lane ranges disjoint
  around the hub so a horizontal route cannot imply a direct connection.
  Member coordinates are local to the
  owning unit diagram; root coordinates place unit instances.
- Reuse the generic Modelica flow animation for electrical networks. A native
  `flow Real` such as `LunCo.Electrical.Pin.i` must be discovered by connector
  metadata and sampled from live node state; Rhai emits ordinary `Pin`/
  `connect(...)` equations and must not grow a generated-electrical animation
  branch. Non-zero signed flow animates in the resolved direction; zero or
  missing state remains idle/diagnostic.
- The flow renderer reads all declared connector flow variables and live
  runtime state keys, not a domain-specific value or generated policy field.
  Precompute lookup keys during projection and keep the per-frame walk linear
  in route segments plus visible dots.
- Keep generated browser metadata explicit: distinguish root boundary inputs and
  outputs from promoted member telemetry, and expose the generated document as
  read-only runtime state with a normal Modelica drill-in path.
- Keep editing semantics honest: an editable Modelica document moves nodes by
  emitting the generic canvas `NodeMoved` event and persisting standard
  `Placement` annotations through `ModelicaOp::SetPlacement`. A generated
  document stays read-only because USD plus Rhai owns its source; expose
  `Duplicate to edit` instead of accepting a non-persistent drag.
- Keep projection responsive: Modelica root loading, parse, and inheritance/icon
  walks run off the UI thread. UI readers use completed caches or a nonblocking
  lock and show an explicit loading/error state until the generic completion
  event requests reprojection. Never hold the engine mutex across painting.
- Validate the returned generated source as a generic strict AST contract:
  exact root name and boundary, required `source`, `units`, `layout.units`,
  `layout.members`, `source_roots`, and `member_output_aliases` fields, no
  undeclared root/unit causal ports, promotions only for outputs present in the
  loaded member class, non-overlapping policy placements, complete policy units,
  native members nested in their owning units, and no direct native members on
  the root. Member-placement overlap is invalid within one unit coordinate
  system; different unit diagrams may legitimately reuse local coordinates.
  Treat missing or loading class definitions as explicit resolver
  states in the canvas, never as a fabricated resolved node.
- Keep generated document lifetime tied to the projection entity. Classify it by
  the `generated/` document origin, retire it on removal/despawn, and keep
  authored document cleanup separate. Structured packages under
  `assets/models/<Root>/package.mo` and Twin-declared `[modelica].paths` are
  ordinary Modelica search-path roots: the compiler/editor discovers the root
  segment of a qualified reference generically and loads that package through
  the shared Modelica engine. A Twin without that section derives package roots
  from its indexed `.mo` files. Each live compile admits required roots through
  the `LoadSourceRoot` worker path before `Compile`; file-backed source assets
  derive requirements from their prepared AST interface, and generated-model
  policies return the required `source_roots` manifest. That manifest does not
  replace composed USD facts for class discovery, and Rust must not name a
  particular library. The live worker rejects unadmitted roots and reports
  failed roots to dependent compiles rather than synchronously rediscovering
  them. Reproject from the generic completion signal.
- Let Rhai own the required `member_output_aliases` promotion table, including
  the explicit empty-table case. Rust may validate known member/output pairs and
  identifier uniqueness, but must not choose aliases or emit visual source for a
  policy.
- Keep policy contracts in authored Rhai scene tests under
  `assets/scenarios/tests/`; reusable standalone assertions may remain under
  `assets/scripting/tests/` and run through `scripts/api/run_rhai_test.sh`.
  The Rust host supplies composed facts and invokes the shipped policy; Rhai
  owns assertions about generated source, topology, layout, and presentation.
  Literal top-level Rhai constants are supported inside policy helper functions
  by the shared hook binding, so presentation policy can remain editable without
  adding Rust-side layout parameters.
- A cyclic set of separately co-simulated components connected by typed scalar
  `SimConnection`s is explicit causal feedback: the fixed-step exchange may
  have a one-step delay, but it is not an unresolved algebraic loop and must not
  emit an algebraic-loop warning. Do not add a Rhai polling bridge. If
  zero-delay continuous feedback is required, synthesize one Modelica network
  and solve it as one system; force-producing cycles remain subject to the
  explicit realtime-safety contract.

## Replace a superseded contract cleanly

When a mechanism is wrong, perform a clean cutover:

1. Identify the authoritative replacement and write a positive test proving
   the replacement contract first. Add a negative case only when rejection or
   safe failure is itself a public contract (for example, the removed spelling
   must be rejected at the public schema boundary).
2. Update source assets, schema source, reader, projection, commands, and docs
   together.
3. Delete the superseded property, alias, compatibility branch, fallback reader,
   and migration-only shim. Do not preserve an invalid contract for compatibility.
4. Regenerate `generatedSchema.usda` and `plugInfo.json` from the authoritative
   source where applicable. Never edit generated schema by hand.
5. Verify parse, composition, contract, projection, solver, and real executable
   behavior. `--validate` proves syntax only; it does not prove runtime wiring.

Permitted defaults are only semantic defaults declared by the authoritative USD
or Modelica schema. A numerical guard such as a positive epsilon is not a
compatibility fallback and must be named as a numerical guard.

## Verification gate

Run the smallest relevant checks first, then the production binary:

```bash
python3 scripts/gen_schema.py
RUSTC_WRAPPER= cargo fmt --all -- --check
python3 scripts/gen_schema.py
"$LUNCOSIM_BIN" test --scene scenes/tests/sensor.usda
RUSTC_WRAPPER= cargo test -p lunco-usd-sim --test usd_connection_mechanics -j 4
CARGO_INCREMENTAL=1 RUSTC_WRAPPER= cargo build -p lunco-luncosim --bin luncosim -j 4
```

For a live feature, launch only `$LUNCOSIM_BIN` with an explicit free
API port, verify readiness, inspect ports and composed connections, and run the
scene through the real executable. Report separately:

- parsed and composed;
- contract and topology accepted;
- solver/runtime behavior observed;
- visual behavior observed; and
- warnings that remain, especially rejected force-loop diagnostics or solver
  warnings.

Keep long USD fixtures and asset-specific assertions in authored `.usda` and
Rhai scene tests. Rust test stages should be minimal and programmatic, and only
cover mechanisms that the production query/command surface cannot observe.

Never claim that a source parse or unit test proves the scene is physically
correct.
