{"always_on":[{"checks":"Always on. The file is a readable RIFF/WAVE with PCM integer (8/16/24/32-bit) or 32-bit float samples, a non-zero sample rate and channel count, and no NaN or infinite samples.","fix":"Export as 16/24-bit PCM or 32-bit float WAV.","id":"artifact.parse","kind":"wav","params":"{}"},{"checks":"Always on. The file is a self-contained glTF 2.0 binary: valid header, embedded buffers and images in the BIN chunk, a strict node tree, finite transforms, and no required Draco / meshopt / quantization / Basis extensions.","fix":"Export as glTF Binary (.glb) with embedded buffers and textures, compression off.","id":"artifact.parse","kind":"glb","params":"{}"},{"checks":"Always on. The upload is a bare URDF (UTF-8 XML, root <robot>) or a zip holding one .urdf (the shallowest if several, at most 4096 entries and 100 MiB unpacked). Every link and joint is named, joint types are URDF types, each geometry has exactly one box, cylinder, sphere or mesh, numbers are finite, and no xacro macros remain.","fix":"Upload a .zip with one .urdf and its meshes, or a bare .urdf; expand xacro first.","id":"artifact.parse","kind":"urdf","params":"{}"},{"checks":"Always on. The clip is UTF-8 CSV in niua.g1-motion/v1: a first line '# niua.g1-motion/v1 fps=<10..1000>', a header naming root_x, root_y, root_z, root_qw, root_qx, root_qy, root_qz and all 29 G1 joints (any order), at least 2 frames and at most 60 s, finite numbers, and a unit root quaternion (w first).","fix":"Export as niua.g1-motion/v1 (see /niua/contracts and docs/explain/embodied-pipeline.md).","id":"artifact.parse","kind":"motion","params":"{}"},{"checks":"Always on. Reported when the server could not run the MuJoCo replay (no simulator configured, a timeout, or a crash). It is the server's fault, not the clip's, and nothing about the clip was judged.","fix":"Retry later, or run the check on a self-hosted NIUA with the simulator installed.","id":"simulation.unavailable","kind":"motion","params":"{}"},{"checks":"Always on. The upload is a zip (at most 64 files, 100 MiB unpacked) with exactly one 3D Gaussian .ply (binary or ascii; vertex properties x, y, z, opacity, scale_0..2, rot_0..3; at most 5,000,000 splats; finite values), exactly one collider (.obj, each o/g group one part, or .stl; at most 500,000 triangles) and optionally niua-splat.json {up_axis, units, reference}. SPZ is not read yet.","fix":"Zip the splat PLY with its collision mesh; export SPZ scenes to PLY first.","id":"artifact.parse","kind":"splat","params":"{}"},{"checks":"Always on. Reported when the server could not run the splat measurement (not configured, a timeout, or a crash). It is the server's fault, and nothing about the bundle was judged.","fix":"Retry later, or run the check on a self-hosted NIUA with the simulator installed.","id":"simulation.unavailable","kind":"splat","params":"{}"},{"checks":"Always on. The upload is a zip (at most 4096 files, 100 MiB unpacked) with one .urdf (the shallowest if several) or, when there is none, one MJCF .xml (root <mujoco>), plus the meshes they reference; or a bare .urdf or MJCF .xml. No entry may point outside the bundle. xacro is not expanded.","fix":"Zip the model file with its meshes, or upload a bare model file; expand xacro first.","id":"artifact.parse","kind":"articulated","params":"{}"},{"checks":"Always on. Reported when the server could not run simcheck (not configured, a timeout, or a crash). It is the server's fault, and nothing about the asset was judged.","fix":"Retry later, or run simcheck yourself (pip install simcheck).","id":"simulation.unavailable","kind":"articulated","params":"{}"},{"checks":"Always on. The scene renders at least one visual triangle (points, lines and collision-only meshes do not count).","fix":"Export the visual mesh: triangulated geometry on nodes not named UCX_* or *-colonly.","id":"artifact.visual_geometry","kind":"glb","params":"{}"}],"rules":[{"checks":"Every node and mesh has a real name, not an exporter default like Cube.001 or Mesh_0.","fix":"Rename the object to what it is in the product (frame, seat, door_L).","id":"naming.no_default_names","kind":"glb","params":"{}"},{"checks":"Each regex in `patterns` matches the name of at least one part: an in-scene node whose subtree renders a mesh, excluding collision-only nodes (a has-collision name like seat-col also matches as seat).","fix":"Model the missing part as its own named object.","id":"parts.required","kind":"glb","params":"{\"patterns\": [\"^frame$\"]}"},{"checks":"Every primitive has a material. Meshes used only by collision-only nodes (UCX_*, *-colonly) are exempt; has-collision nodes (*-col) are visual and are not.","fix":"Assign a material slot to the mesh before export.","id":"materials.assigned","kind":"glb","params":"{}"},{"checks":"Materials whose name contains a `name_patterns` word use KHR_materials_transmission with factor > 0. Patterns match whole words in the material name (split on _, -, ., space, case changes and digits); name materials like window_glass or WindowGlass. Patterns are plain words, not regexes; Glassware does not match glass.","fix":"Set Principled BSDF Transmission Weight to 1.0; glass is transmission, not a tinted alpha.","id":"materials.glass_is_transmissive","kind":"glb","params":"{\"name_patterns\": [\"glass\", \"stained_glass\"]}"},{"checks":"Materials whose name contains a `name_patterns` word have metallicFactor >= 0.5 (a metallic-roughness texture can only lower it). Patterns match whole words in the material name (split on _, -, ., space, case changes and digits); name materials like frame_metal or FrameMetal. Patterns are plain words, not regexes; Environment does not match iron.","fix":"Set Metallic to 1.0 on the metal material.","id":"materials.metal_is_metallic","kind":"glb","params":"{\"name_patterns\": [\"metal\"]}"},{"checks":"All triangles in the scene <= `max`, collision-only meshes included (some engines render UCX_ or -colonly); each visual part's triangles <= `per_part_max` when set. Metrics: triangles (visual), collision_triangles, total_triangles.","fix":"Decimate or remove hidden geometry on the heaviest part.","id":"budget.triangles","kind":"glb","params":"{\"max\": 20000, \"per_part_max\": 8000}"},{"checks":"Every image's longer side <= `max` pixels.","fix":"Resize the texture or re-bake at the budget resolution.","id":"budget.texture_px","kind":"glb","params":"{\"max\": 2048}"},{"checks":"At least one in-scene mesh node is collision. Collision-only (not rendered): UCX_/UBX_/UCP_/USP_ prefix, -colonly / -convcolonly suffix. Has-collision (visual mesh kept): -col / -convcol / _col suffix, or extras.collision = true. Collision-only meshes count toward the triangle total (budget.triangles max) and the bounds max; they are excluded only from per_part_max, materials.assigned, parts.required, the visual bounds min and the parts metric.","fix":"Add a simple convex collision mesh named UCX_<part> (or <part>-colonly for Godot).","id":"collision.present","kind":"glb","params":"{}"},{"checks":"World bounding box size (meters) per axis: the visual meshes are >= `min`, and the visual plus collision-only meshes are <= `max`. The bounds_m metric is the visual box.","fix":"Apply scale and model at real-world size in meters.","id":"scale.bounds_m","kind":"glb","params":"{\"min\": [0.05, 0.05, 0.05], \"max\": [10, 10, 10]}"},{"checks":"No node has a mirrored or degenerate transform (determinant of its local matrix <= 0, e.g. one negative or zero scale axis). Two negative scale axes are a rotation and pass.","fix":"Apply transforms; mirror with a modifier instead of negative scale.","id":"transform.no_negative_scale","kind":"glb","params":"{}"},{"checks":"Sample rate is one of `allowed`.","fix":"Resample to an allowed rate.","id":"format.sample_rate","kind":"wav","params":"{\"allowed\": [44100, 48000]}"},{"checks":"Channel count is one of `allowed`.","fix":"Downmix or upmix to an allowed channel count.","id":"format.channels","kind":"wav","params":"{\"allowed\": [1, 2]}"},{"checks":"Duration in seconds is within [min, max].","fix":"Trim or extend the clip to the allowed length.","id":"duration.range_s","kind":"wav","params":"{\"min\": 0.1, \"max\": 10}"},{"checks":"Sample peak stays below `max_dbfs` (default -0.3).","fix":"Lower the gain or apply a limiter with a ceiling below the threshold.","id":"level.no_clipping","kind":"wav","params":"{\"max_dbfs\": -0.3}"},{"checks":"Integrated loudness (EBU R128) is within [min, max] LUFS. Needs >= 0.4 s of audio.","fix":"Normalize loudness to the target window.","id":"level.loudness_lufs","kind":"wav","params":"{\"min\": -24, \"max\": -12}"},{"checks":"Leading and trailing silence (below -60 dBFS) each last <= `max_ms`.","fix":"Trim leading and trailing silence.","id":"silence.edges_ms","kind":"wav","params":"{\"max_ms\": 50}"},{"checks":"The links and joints form one tree: unique link and joint names, every joint's parent and child are links in the file, no link has two parent joints, no joint joins a link to itself, exactly one root link, and every link is reachable from it.","fix":"Point each joint at existing links and connect every link to the base with exactly one parent joint (fixed if it does not move).","id":"tree.valid","kind":"urdf","params":"{}"},{"checks":"Every link that has geometry or is the child of a movable joint has an <inertial> with mass >= `min_kg`. Geometry-less frames on fixed joints (world, tool0) are exempt.","fix":"Give the link its real mass in kg (from CAD, or density × volume).","id":"inertial.mass_positive","kind":"urdf","params":"{\"min_kg\": 0.001}"},{"checks":"Every <inertia> is physically possible. On its principal moments (eigenvalues of the symmetric tensor): all > 0 (positive definite), I1 + I2 >= I3 (triangle inequality), and none above mass × d², where d is the model's bounding-box diagonal (catches placeholder identity tensors). Relative tolerance 1e-6.","fix":"Recompute the tensor from the link geometry (a solid box of mass m and size a, b, c has ixx = m(b² + c²)/12).","id":"inertial.tensor_valid","kind":"urdf","params":"{}"},{"checks":"Every link with a <visual> has at least one <collision>. When `max_mesh_triangles` is set, each collision mesh has at most that many triangles (the render mesh is not a collider).","fix":"Add a <collision> with a primitive or a convex hull of the visual mesh.","id":"collision.on_visual_links","kind":"urdf","params":"{\"max_mesh_triangles\": 5000}"},{"checks":"Every revolute and prismatic joint has <limit> with lower < upper, effort > 0 and velocity > 0. Every movable joint except floating has a non-zero <axis>.","fix":"Set lower, upper, effort and velocity from the actuator's datasheet (radians or metres; N·m or N; rad/s or m/s).","id":"joints.limits","kind":"urdf","params":"{}"},{"checks":"Every <mesh filename> resolves inside the upload (relative to the URDF, or package://<pkg>/<path> against the bundle) and loads: STL (binary or ASCII), OBJ, COLLADA (.dae) or GLB with at least one triangle. Absolute paths and URLs never resolve; the gate reads nothing outside the upload.","fix":"Ship each mesh in the zip at the referenced path, or re-export it as binary STL, OBJ or GLB.","id":"meshes.resolve","kind":"urdf","params":"{}"},{"checks":"The model's size (largest side of the bounding box of all visual and collision geometry, mesh scale applied, at the zero joint configuration) is within [`min_m`, `max_m`]. A model about 1000× too large is reported as millimetres.","fix":"Model in metres; for millimetre meshes set scale=\"0.001 0.001 0.001\" on <mesh>.","id":"scale.metres","kind":"urdf","params":"{\"min_m\": 0.02, \"max_m\": 20}"},{"checks":"Every joint of the clip stays inside its range in the MuJoCo Menagerie G1 (g1.xml), by more than `margin_rad` never. Reported per joint with the worst frame. Simulation only, no hardware tested.","fix":"Clamp the joint to its range, or retarget so the pose needs less of it.","id":"joints.position_limits","kind":"motion","params":"{\"margin_rad\": 0.0}"},{"checks":"Each joint's speed (finite differences at the clip's fps) is at most `max_ratio` × Unitree's published velocity limit for that joint (g1_29dof URDF).","fix":"Retime a fast move, or remove a one-frame pop with a short median filter.","id":"joints.velocity_limits","kind":"motion","params":"{\"max_ratio\": 1.0}"},{"checks":"Replayed kinematically frame by frame in MuJoCo, no robot geometry goes deeper than `max_m` into the floor.","fix":"Lift the root on those frames so the lowest foot point rests on the floor.","id":"contact.ground_penetration","kind":"motion","params":"{\"max_m\": 0.01}"},{"checks":"Replayed kinematically, no two robot links overlap deeper than `max_depth_m` (MuJoCo convex hulls of the collision meshes; adjacent links are excluded). Hulls overstate concave links, so the default allows 1 cm.","fix":"Move the limb clear of the body on those frames.","id":"contact.self_collision","kind":"motion","params":"{\"max_depth_m\": 0.01}"},{"checks":"Each foot contact point (heel and toe spheres) that is within `contact_height_m` of the floor on two consecutive frames slides at most `max_mps` horizontally.","fix":"Lock the planted foot: remove the root drift on frames where the foot is on the floor.","id":"feet.skate","kind":"motion","params":"{\"max_mps\": 0.2, \"contact_height_m\": 0.02}"},{"checks":"The clip is replayed in MuJoCo on CPU with joint PD tracking (the model's own position actuators, root unactuated, the model's timestep) from its first frame. Sampled at 50 Hz, the pelvis stays above `min_pelvis_m` and within `max_tilt_deg` of vertical for the whole clip, and the state stays finite. There is no balance controller, so this passes quasi-static clips and fails most dynamic ones. The receipt records the MuJoCo version, timestep, integrator and gains.","fix":"Slow the clip down, or keep the centre of mass over the feet. Or repair it (POST /api/v1/repair, MCP repair_motion, niua-runner repair clip.csv): balance is added in this same replay, and the receipt names the repair the clip claims.","id":"replay.upright","kind":"motion","params":"{\"min_pelvis_m\": 0.55, \"max_tilt_deg\": 25}"},{"checks":"The splat holds at most `max` Gaussians.","fix":"Prune the lowest-opacity and smallest splats, or split the scene.","id":"budget.splat_count","kind":"splat","params":"{\"max\": 5000000}"},{"checks":"The collider's floor (area-weighted normal of its up-facing triangles) is within `max_deg` of the declared up axis (niua-splat.json up_axis, default +z). On a fail the report gives the quaternion that levels it.","fix":"Rotate the splat and the collider together by the reported quaternion, or declare the right up_axis.","id":"frame.gravity_aligned","kind":"splat","params":"{\"max_deg\": 2}"},{"checks":"Opaque splat centres (opacity >= `opacity_min`) lie within a median `max_median_m` of the collision mesh's surface. On a fail the gate fits the transform that puts the splat on the collider (the 24 axis-aligned rotations, then trimmed ICP; axis-aligned rotations and unit conversions are reported exactly) and prints it.","fix":"Apply the reported transform to the splat (p' = scale × R(q) × p + t), or export both from the same scene in the same frame and units.","id":"frame.matches_collider","kind":"splat","params":"{\"max_median_m\": 0.03, \"opacity_min\": 0.5}"},{"checks":"The collider is between `min_m` and `max_m` across (diagonal of its bounding box, in the declared units). With a reference in niua-splat.json ({from, to, metres}), the measured distance must match the declared metres within `reference_tolerance`; only then is the scale exact. Without one the receipt says plausible only.","fix":"Export in metres or declare the units; add a reference measurement (two points and the real distance) to make the scale exact.","id":"scale.plausible","kind":"splat","params":"{\"min_m\": 0.2, \"max_m\": 500, \"reference_tolerance\": 0.02}"},{"checks":"At most `max_ratio` of opaque splats float: farther than `distance_m` from the collider and sparse (fewer than a quarter of the splat's median neighbour count within distance_m). Dense splats over a collider gap count against collision.coverage instead.","fix":"Delete splats farther than distance_m from the collision mesh (a crop box or a distance filter in the splat editor).","id":"quality.floater_ratio","kind":"splat","params":"{\"max_ratio\": 0.02, \"distance_m\": 0.1}"},{"checks":"Projected on the floor plane in `cell_m` cells, at least `min_ratio` of the cells the opaque splat occupies (3 or more splats) have collision geometry. The report samples uncovered cells.","fix":"Add collision geometry where the splat shows surfaces but the collider has none.","id":"collision.coverage","kind":"splat","params":"{\"min_ratio\": 0.95, \"cell_m\": 0.1}"},{"checks":"`probes` spheres (5 cm) are dropped onto covered cells in MuJoCo on CPU (gravity along -up, 2 s) and each must come to rest within `max_rest_error_m` of the real collider surface below it. MuJoCo collides each collision part (one OBJ group) as its convex hull, so a concave one-piece room holds probes above the floor; that fails here. Simulation only: says nothing about real friction.","fix":"Split the collider into convex parts (one OBJ group each, for example with CoACD) and close any holes.","id":"collision.drop_test","kind":"splat","params":"{\"probes\": 16, \"max_rest_error_m\": 0.03}"},{"checks":"simcheck (Apache-2.0) loads the URDF or MJCF in MuJoCo on CPU and runs it: the file and kinematic tree, mass and inertia (against size and against the part's own geometry), collision against visual, concave meshes MuJoCo collides as convex hulls, joint axes and limits, contacts at rest, joint sweeps and drops. Every finding at `fail_on` severity (error, or warning too) fails, and so does any finding from a check listed in `also_fail` whatever its severity; each failure carries simcheck's check id and its own fix text. The built-in also fails a visible moving part with no collision (geometry.collision), collision far from or far smaller than its visual (geometry.visual_match) and placeholder inertia (inertia.geometry), which simcheck rates info or warning. Simulation only, no hardware tested.","fix":"Fix what the finding says; `simcheck explain <check>` gives the full explanation.","id":"simcheck.no_errors","kind":"articulated","params":"{\"fail_on\": \"error\", \"also_fail\": [\"geometry.collision\", \"geometry.visual_match\", \"inertia.geometry\"]}"},{"checks":"Every simcheck check ran: no engine timed out and no check crashed. A partial run is not a pass.","fix":"Reduce the model (fewer or simpler collision meshes) so MuJoCo can load and run it in time.","id":"simcheck.complete","kind":"articulated","params":"{}"}]}