--- name: validate-assets description: > Pre-flight a LunCoSim `.mo`, `.usda`, `.sysml`, `.kerml`, `.wgsl`, or `.rhai` asset with `ValidateAsset` or the production CLI, or inspect one Twin's resolver namespaces with `ValidateTwin`. Use for parse, reference, schema, shader-parameter, Modelica, SysML, namespace, or authored-lint checks. These are read-only queries; use `RunLint` for a loaded scene and test-via-api for runtime behavior. --- # Validate an asset (pre-flight) `ValidateAsset` answers one question — **does this file parse, and would the engine accept it?** — without mounting a scene or starting simulation. Runtime queries prepare fresh source facts asynchronously, then apply the authored lint policy serially in the captured runtime context. Browser preparation uses the cooperative executor; it does not promise a separate CPU worker. Implementation: [`crates/lunco-scene-validation/src/validate.rs`](../../crates/lunco-scene-validation/src/validate.rs). Related: [`author-usd-component`](../author-usd-component/SKILL.md) (author the file), [`use-asset-library`](../use-asset-library/SKILL.md) (get it discovered), [`build-vehicle`](../build-vehicle/SKILL.md) (wheels), [`test-via-api`](../test-via-api/SKILL.md) (drive the running app once it validates), and [`sysml-requirements`](../sysml-requirements/SKILL.md) (Twin source sets and verification cases). ## Two invocation forms ### CLI — no app, no window, no GPU ```bash "$LUNCOSIM_BIN" --validate \ assets/models/LunCo/Electrical/Battery.mo \ assets/vessels/rovers/skid_rover.usda \ assets/shaders/rover_hull.wgsl \ requirements/system.sysml ``` The flag is intercepted in `crates/lunco-luncosim/src/bin/luncosim.rs` **before** the Bevy `App` is built, and the process `exit`s — nothing is rendered, no window opens, no port is bound. Run it anywhere, any time. For a WGSL file, this pre-flight validates the file's standalone source/schema contract. It cannot decide whether the file is a fragment or vertex stage when it is not referenced by a USD material. That role is authored by the USD `Shader` prim (`info:wgsl:sourceAsset` and optional `info:wgsl:vertexAsset`) and is checked by the production USD/render boundary. Use an authored USD + Rhai scene test for that contract; do not add a Rust test that reads a repository shader path to simulate it. This ownership rule applies to every asset type: if a test names a shipped or Twin asset, author the fixture and assertion beside the asset as USD/Rhai and observe it through `ValidateAsset` or the live API. Calling `lunco-assets-core`/`lunco-storage` from a Rust test is the correct access path for an asset-owning mechanism, but it does not turn an authored asset contract into a Rust mechanism test. Rust fixtures should be inline or temporary and should test only the generic parser, resolver, or storage behavior. | Exit code | Meaning | |---|---| | **0** | every report `ok` | | **1** | at least one report failed | | **2** | `--validate` given with no paths | - **Multiple paths**: everything after `--validate` up to the first argument starting with `--`. - **Exact flag match only** — `--validate=path` and `-v` are not parsed. - Output per file: `OK ()` / `FAIL ()`, then indented `error:` and `warning:` lines on stdout. ### API — against a running luncosim ```bash curl -s -X POST http://127.0.0.1:4101/api/commands \ -H "Content-Type: application/json" \ -d '{"type":"ExecuteCommand","command":"ValidateAsset","params":{"path":"lunco://models/LunCo/Electrical/Battery.mo"}}' ``` The initial request admits one operation and returns `{"state":"pending","operation_id":N}`. Poll the same query with only `{"operation_id":N}` until it returns `ready` or `failed`. `ready` contains `report` and `source_revisions` from the actual reads; `failed` contains its actual `diagnostic`. Both terminal results are consumed once. Unknown, consumed, or retired operation IDs reject visibly. Never resubmit `path` while waiting: that would admit a different operation. ```bash curl -s -X POST http://127.0.0.1:4101/api/commands \ -H 'Content-Type: application/json' \ -d '{"type":"ExecuteCommand","command":"ValidateAsset","params":{"operation_id":1}}' ``` Use the returned ID in place of `1`. Admission is bounded by the shared `AsyncWorkAdmission`; running tasks and unconsumed results retain their permit. `lunco-scene-validation` registers the providers. The CLI remains a native synchronous entry point over the same validators. ## The report ```json {"path":"…", "kind":"modelica|usd|sysml|wgsl|rhai|unknown", "ok":true, "errors":[], "warnings":[], "info":{}} ``` Runtime `ready.report` contains this shape; native CLI reports use it directly. `ok == errors.is_empty()`. **Warnings never fail a file.** `path` echoes what you passed, *not* the resolved disk path — if you need to know which file was read, pass an unambiguous one. ### Twin-wide namespace pre-flight Use `ValidateTwin` when the question spans the complete Twin rather than one file. It reads the indexed Twin folder and compares only names that share a real resolver namespace and scope. The report includes each entry's owner, source, domain, scope, and resolution rule, plus the complete collision group. Equal names in independent Modelica roots, asset directories, or USD stages are not reported. ```bash curl -s -X POST http://127.0.0.1:4101/api/commands \ -H 'content-type: application/json' \ -d '{"type":"ExecuteCommand","command":"ValidateTwin","params":{"path":"/work/rover-twin","policy":"error"}}' ``` `path` is required: use the current `twin://` for a mounted Twin on either platform; native callers may also supply a directory or standard file URI. Browser native-directory inspection is not available. Poll its returned operation ID using the same protocol as `ValidateAsset`. `policy` is optional: `warn` (the default) makes namespace collisions warnings; `error` makes those collisions fail the report. Source-read failures fail the prepared report. The query does not rename files or choose a winner. Its Rhai policy is `assets/scripting/policy/lint_twin.rhai`, so a running session can replace `lint.twin` for the next explicit check. For the active Twin in a running scene, use the live command instead: ```rhai cmd("RunLint", #{scope: "twin", policy: "warn"}); query("GetDiagnostics", #{scope: "twin"}); ``` `RunLint` and `ValidateTwin` share namespace facts and Rhai policy. Runtime `ValidateTwin` captures indexed source identity, owner, mount, and policy at admission and rejects publication after retirement or indexed source-set changes. Twin `RunLint` also uses the shared async preparation owner on native and mounted browser sources. Its queued Ack identifies the lint revision; inspect `GetDiagnostics` until that scope is ready or failed. Source failures fail the scope independently of collision severity. Captured owner, mount, scene, policy, and Rhai context fence publication; superseded or retired work is discarded. Loaded-stage lint works on either platform. For a composed document, `ValidateAsset` is still the file-level gate. Use the live Rhai `model_authoring` facades for the next question: whether the exact assembly is understandable and ready to use. Call `model_context` first, then `readiness_report` with the Twin's explicit policy; use `port_graph`/ `wiring_plan` for cross-domain endpoints and `publish_component` before an explicit Save-As. These facades validate composed document identity and generation and return dry plans; they do not replace `ValidateAsset`, mutate the stage, or silently save. See the [model-authoring guide](../../docs/scripting-guide.md#model-and-assembly-authoring-human-and-ai). ## What each extension actually checks | Ext | Checks | Can it FAIL? | |---|---|---| | `.mo` | rumoca `parse_to_syntax` + AST facts + authored `lint.modelica` policy | yes | | `.sysml`/`.kerml` | SysML parser/resolver + typed requirement/verification facts + authored `lint.sysml` policy | yes | | `.usd`/`.usda`/`.usdc` | byte-layer preparation → **compose the reference closure** → strict `WheelParams::read` on every `PhysxVehicleWheelAPI` prim | yes | | `.wgsl` | `ParamSchema::parse` — reflect the `struct Material` uniform + `//!@` annotations | **no** — warnings only | | `.rhai` | `rhai::Engine::new().compile()`, nothing executed | yes | | anything else | `unsupported extension` error | yes | USD extensions enter the same byte-layer owner. Text layers compose normally. Binary USDC currently fails the dependency inspector's UTF-8/text-layer boundary with a diagnostic; recognizing its extension does not establish binary composition support. For a Twin source set, use `ValidateSysml` for source status, diagnostics, and source identity; use `AnalyzeSysml` when a policy needs typed semantic facts. Manifest bindings are a separate active-Twin contract read and are joined with SysML identities by Rhai policy. See [`sysml-requirements`](../sysml-requirements/SKILL.md). ### `.mo` — the branch-free policy is the point Rumoca's solver path is branch-free. `ValidateAsset` parses once through the shared `lunco-modelica-ast` boundary, passes AST-derived declaration and conditional-construct facts to the reloadable `lint.modelica` Rhai policy, and returns its findings as errors. The policy covers conditional expressions, structural equation branches, and algorithm `if`/`when` constructs. An `if` in a binding or modifier is not an equation/algorithm construct and is not reported. Fix a reported branch by rewriting the model with branch-free arithmetic such as `max()`/`min()`. Battery, network, and brownout equations belong in Modelica, not a tick script. `info` carries `{model, params, inputs, outputs:null}`. `outputs` is always `null` — outputs are not knowable before a compile. > **Lint boundary:** parse failures remain visible as parser errors. Recovered > AST facts are best-effort and carry `kind = "unknown"` when declaration > ownership cannot be proven; the policy reports that case as a warning rather > than guessing. ### `.usda` — this is the one that catches broken references Three stages, first failure short-circuits: 1. `usda_to_data` — this file's own syntax. 2. Shared USD recipe preparation — **fetches the whole layer closure** (`subLayers` + `references` + `payload`, including arcs inside variant blocks). A dangling `@lunco://…@` is a hard error here. This is the single best reason to run it: [bare paths silently no-load at runtime](../use-asset-library/SKILL.md#the-lunco-scheme), but a *missing* target fails loudly right here. 3. `WheelParams::read` on every prim with `PhysxVehicleWheelAPI` — the **same strict reader the spawner uses**. The error names every missing attribute: `wheel /Rover/Wheel_FL would refuse to spawn — missing required attributes: …` `info.wheel_prims` lists each wheel with `ok` and, when failing, `missing`. > Three things it does **not** catch: binary leaf references (`.glb`/`.obj`/`.stl` > are not layers, so a broken mesh path passes); suspension-inherited wheel > attrs — the reader is called with no attachment suspension, so a wheel that > only validates once its suspension arc composes at spawn time is judged > without it; and **collider relationships**, which are where many mechanism > bugs live. The composed USD lint does catch an unowned renderable vehicle > gprim and an enabled collider shape that the runtime reader cannot build. > > That remaining limit is worth knowing. The static lint cannot see that two > colliders on the same vehicle overlap, or that a strut hangs lower than the > foot that is supposed to carry it — facts about assembled motion and contact, > not just ownership and shape support. Clearance is a **runtime** check: run > the scene under `luncosim test` and assert the mechanism moved (see > [`author-usd-physics`](../author-usd-physics/SKILL.md#2-a-prismatic-joint-carries-moment)). > A vehicle can pass static validation and still land on its shins. ### `.wgsl` — cannot fail, read the warnings There is **no naga validation** — deliberately. A syntactically broken shader that still contains a parsable `struct Material` reports `ok: true`. What you get is the reflected param schema (`info.shader_params` with `name`/`type`/`offset`/ `ui`/`default`, plus `uniform_size`) and two possible warnings: - `no reflectable Material struct` — the shader exposes no tunable params and cannot be driven by `SetObjectProperty`. - `not prop-pickable: engine fields beyond sun_vis` — it uses `//!@engine` params only the terrain pipeline fills, so the prop-material picker skips it. It still works as a scene shader. See [`use-asset-library` § Shaders](../use-asset-library/SKILL.md#add-a-shader-wgsl). ## Source admission and path resolution Runtime logical `lunco://` and current `twin://` addresses use the registered Bevy asset reader and the shared USD dependency-closure reader. Literal `#`, `%`, and Unicode path characters retain their typed asset identity. Native paths and standard file URIs use captured `FileDocumentAdmission` and bounded storage reads on the preparation task, then validate exact ownership before publication. A raw native path is checked as given before native engine-root resolution; prefer a root-qualified logical address to avoid CWD ambiguity. Browser callers use registered logical sources; unsupported unmounted USD file sources reject explicitly. Native CLI resolution remains native path first, then the engine asset owner; it has no live Twin mount registry. The host may configure the public `QueryPreparationLimits` resource. Its `StageClosureLimits` bound files, depth, and bytes; query reads are serial by default. Twin preparation passes its remaining aggregate budget into each closure before any dependency read. A completed query records actual source content revisions, rather than treating dispatch-time paths as fresh reads. ## The rules are authored — the lint layer Everything above is what the **loader** would refuse: parse, compose, `WheelParams`. Compiled, because it is the loader's own code. A second tier runs on the same call and answers a different question — **is this right?** Those rules live in `assets/scripting/policy/lint_.rhai` and are reached through the `lint.` hook, so adding, tightening or silencing one is an edit to a script, not a rebuild. Findings arrive in the same report: `error` severity joins `errors` (and flips `ok`), everything else joins `warnings`. Each line is prefixed with its domain and rule id, which is what you grep for: ``` [usd/nested-body-no-joint] /Rover/Motor_FL — applies PhysicsRigidBodyAPI inside the body but no joint names it — it is a SEPARATE body held by nothing and will fall out of the vehicle. … ``` The USD rules include `nested-body-no-joint` (error), `joint-target-not-a-body` (error), `collision-enabled-without-api` (error for ordinary geometry; wheel realizations use their dedicated contract rules), `raycast-wheel-collision-contract` (error), `physical-wheel-collision-contract` (error), `vehicle-part-collision-contract` (error), `dynamic-body-no-collider` (warn), `mass-outside-any-body` (warn), `conditionally-stable-joint-drive` (error), `joint-drive-negative-stiffness` (error), `joint-drive-negative-damping` (error), `invalid-gear-drive` (error), and `invalid-network-synthesizer` (error). Collection ownership is derived from the composed member role schemas by the same runtime classifier; an absent selector does not make a physical actuator collection a Modelica network. See [`author-usd-physics`](../author-usd-physics/SKILL.md#6-a-part-is-not-a-body) for the authoring rule they enforce and [`docs/architecture/lint-substrate.md`](../../docs/architecture/lint-substrate.md) for the design. ### The file is not the scene — `RunLint` `ValidateAsset` lints a **file**. After a scene is loaded, spawned into and edited, no file describes what is running; lint **that** with the verb: ```rhai cmd("RunLint", #{}); // explicit loaded-stage lint query("GetDiagnostics", #{scope: "loaded_stages"}); // poll complete; read diagnostics[] ``` or `{"type":"ExecuteCommand","command":"RunLint"}` over HTTP/MCP. Nothing lints automatically at load, on a physics tick, or on a background cadence — deliberately. An editor, launcher, or caller explicitly repeats the command after an authored change, and ```rhai bind_policy("lint.usd", "lint_usd", my_rules); // next RunLint obeys ``` re-shapes the rules for the next explicit lint run without a rebuild. Loaded lint evidence can be deferred for one or more frames; wait until `complete:true` before using `ok:true` as a clean result. `RunLint` also checks the live projected port surface through the shared `PortRegistry`, passing its owner collision facts to the authored `lint.usd` policy. A `port-owner-collision` error identifies the composed entity, `inputs:`/`outputs:` path, owner source/domain/backend, and the actual registry precedence that wins reads or writes. Repeated inspection views of one owner are deduplicated. This is live-only: `ValidateAsset` cannot see runtime owners that are introduced by projection. Repair it in USD/Rhai authoring so one semantic public name has one owner; rename a separate actuator (for example to `dock_release`) instead of adding retries, fallbacks, or vehicle-specific Rust input handlers. USD connections have a second, explicit preflight contract. Static file lint reports invalid property paths, missing source prims/properties, and authored type mismatches. Loaded `RunLint` additionally checks runtime-provider connections against the exact projected `inputs:`/`outputs:` port name and reports a missing port as an error or an explicitly pending surface as a warning. A runtime source without the standard direction namespace is an error. Rust only supplies these facts; the Rhai USD policy chooses rule IDs, severity, and wording. `ValidateAsset` cannot prove dynamic runtime names. Control-binding validity is projected from the canonical `lunco_control_core::parse_user_intent` parser and reported by the same Rhai USD policy. The Rust validator does not maintain a second intent spelling list or produce policy wording; unknown bindings are surfaced as `control-binding-unknown-intent` findings. ## Where it fits ``` edit .usda / .mo / .wgsl / .rhai ↓ --validate ← seconds, no GPU. Catches: syntax, broken refs, missing ↓ wheel attrs, if/when in Modelica, unparsable rhai. load the scene ← test-via-api ↓ drive it / assert ← author-scenario, drivetrain_parity ``` Validate **every file you touched** before you launch anything. A `--validate` run costs seconds; a luncosim launch that dies on a typo costs a compile. ## Anti-patterns - ❌ Launching the full luncosim to find out whether a file parses — that is what `--validate` is for. - ❌ Sending `ValidateAsset` to **lunica** and concluding the command doesn't exist. It is luncosim-only; use the CLI. - ❌ Treating a `.wgsl` `ok: true` as "the shader compiles" — no naga runs. Only a real load proves the pipeline builds. - ❌ Passing a bare `models/X.mo` from an arbitrary CWD and trusting which file was read — `path` in the report echoes your input, not the resolved path. - ❌ Passing a `twin://` address — unresolvable; use the filesystem path. - ❌ Reading `ok` and ignoring `warnings` on a `.wgsl` — that file can never report `ok: false`, so the warnings ARE the result. - ❌ Adding an `if` to a `.mo` equation section to "handle a case" — rewrite it branch-free; the lint is enforcing a real solver constraint, not a style rule. - ❌ Reading a `[usd/…]` lint error as "the file is broken syntax". It parsed and composed fine — it says the file would load and then behave wrongly. Fix the authoring, don't chase the parser. - ❌ Validating a file and concluding the running scene is clean. Runtime spawns and edits are in no file; `cmd("RunLint", #{})` is the check for those. - ❌ Adding a rule in Rust. Rules go in `assets/scripting/policy/lint_*.rhai`; only new FACTS are Rust, and only when no existing fact can answer the question (`facts.prims[].schemas` answers most of them).