Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

animsmith CLI

animsmith is designed for artist inner loops, CI gates, and pipeline automation. It reads glTF/GLB everywhere; the released default build also reads FBX through the fbx feature. The CLI is tested on Linux, macOS, and Windows.

Install

Install the released CLI by downloading the archive for your platform from GitHub Releases:

PlatformArchive
Linux x86_64animsmith-vX.Y.Z-x86_64-unknown-linux-gnu.tar.gz
macOS Apple Siliconanimsmith-vX.Y.Z-aarch64-apple-darwin.tar.gz
Windows x86_64animsmith-vX.Y.Z-x86_64-pc-windows-msvc.zip

Each archive has a matching .sha256 checksum file.

You can also install from crates.io with Cargo:

cargo install animsmith

From a source checkout, prefix commands with cargo run -p animsmith --.

Help

Every command has generated help:

animsmith --help
animsmith lint --help
animsmith fix --help
animsmith generate addressability --help
animsmith generate contact-fragment --help

There are no man pages yet, so --help is the canonical installed CLI reference. The help output reflects compile-time features: a --no-default-features binary omits feature-gated commands such as report, convert, and assemble.

animsmith --version states that same compile-time surface in one line:

animsmith <version> (<git describe of the source>) [features: fbx, report]

The parenthesized part appears only for a binary built from a checkout whose commit is not exactly the release tag. [features: ...] names the Cargo features that were compiled in, alphabetically, and reads [features: none] for a --no-default-features build: fbx means the binary reads FBX input, report means it can render HTML reports. The line is a label the build stamps on itself, so treat it as identification rather than as proof — run the command you depend on when a build's capabilities have to be established.

Commands

animsmith inspect <file>
animsmith measure <file...> [--format text|json]
animsmith lint <file...> [--format text|json|markdown] [--select id[,id]] [--allow id[,id]] [--deny-warnings]
animsmith evaluate-transition-poses <input.glb|input.gltf|input.fbx> [--config animsmith.toml] --format json
animsmith collection lint <collection.toml> [--format json]
animsmith collection dashboard --collection <collection-output.json> -o <dashboard.html> --authority <dashboard.json> [--evaluation <transition-pose-evaluation.json>] [--asset-report <logical-id>=<relative-report.html>]
animsmith collection generate-contact-fragment <manifest.toml> --clip <logical-id> -o <out.json> [--format text|json]
animsmith collection evaluate-directional-speed --policy <policy.toml> --evidence <collection-output.json> [--format json]
animsmith collection evaluate-transition-poses <collection.toml> --families <transition-families.toml> --format json
animsmith report <file> -o <report.html> [--clip name] [--evidence-only]
animsmith report <before> -o <comparison.html> --compare-after <after> --before-clip <before-name> --after-clip <after-name> [--evidence-only]
animsmith transform <file> -o <out.glb> [--clip name] [--slice START:END] [--hold-extend SECONDS] [--gait-anchor] [--drop-duplicate-loop-endpoint] [--prune-constant-tracks] [--fps N]
animsmith fix <file> (-o <out.glb>|--in-place|--dry-run) [--repair id[,id]]
animsmith convert <in.fbx|in.glb|in.gltf> -o <out.glb|out.gltf> [--material-texture-recipe recipe.toml] [--animation-only|--bake-static-mesh-transforms] [--format text|json]
animsmith assemble <recipe.toml> -o <out.glb> --evidence <out.json> [--format text|json]
animsmith scale whole-document <in.glb|in.gltf> -o <out.glb|out.gltf> --factor N --evidence <out.json> [--format text|json]
animsmith scale rest-bind <in.glb|in.gltf> -o <out.glb|out.gltf> --source-skin-index N --source-root-node-index N --expected-factor N --evidence <out.json> [--format text|json]
animsmith generate addressability <in.glb|in.gltf> [--format json|text|markdown]
animsmith generate contact-fragment <in.glb|in.gltf|in.fbx> --clip <take-name> -o <out.json> [--format text|json]
animsmith diff <before> <after> [--format text|json]
animsmith skeleton compare <source.glb|source.gltf|source.fbx> <target.glb|target.gltf|target.fbx> --correspondence <skeleton-correspondence.toml> [--format text|json]

inspect is the human-readable discovery view for exact asset-authored names. It inventories clips, bones, materials, and mesh-instance nodes, including each instance's mesh, skin status, and primitive/material context. Use those names when authoring assemble or material texture recipes.

Structural skeleton comparison

skeleton compare establishes only declared structural compatibility evidence for one source asset and one immutable target asset. It does not retarget, rewrite rest or bind data, infer aliases, generate an engine avatar, or prove runtime deformation, masking, contact, gameplay, or artistic acceptance.

The correspondence control is strict and identity-pinned. Generate its two input identities from the exact source and target bytes you intend to compare; a changed asset is a stale control input and exits 2 without a result. The selector declares the complete finite node-name set on each side; it does not infer a subtree from root_name. The named root must be included, every name must resolve exactly once in its input, and only these explicitly selected nodes contribute rows. This prevents meshes, props, cameras, and other scene attachments from silently becoming skeleton evidence. Parent comparison walks through unselected intermediate nodes to the nearest selected ancestor, so it describes this declared projection rather than the complete raw hierarchy. All tolerances are mandatory and have no defaults:

schema = "urn:animsmith:skeleton-correspondence:1"
schema_version = 1

[source]
input = { sha256 = "<64 lowercase hex characters>", bytes = 12345 }
selector = { root_name = "Hips", node_names = ["Hips", "Spine", "Head"] }

[target]
input = { sha256 = "<64 lowercase hex characters>", bytes = 23456 }
selector = { root_name = "Hips", node_names = ["Hips", "Spine", "Head"] }

[correspondence]
mode = "exact_name"

[tolerances]
translation_m = 0.001
rotation_deg = 0.1
scale_delta = 0.001
normalized_bone_length_ratio_delta = 0.01

For a deliberate rename, replace the correspondence table with a finite, one-to-one explicit map. Every map key and value must already be in the respective declared selector; the map is authority, not a suggestion:

[correspondence]
mode = "explicit"
map = { Hips = "Root", Spine = "Spine_01" }

The versioned urn:animsmith:schema:skeleton-compatibility:1 JSON result binds both primary inputs, complete dependency closures when they are available, the correspondence bytes, normalized basis, selectors, matching mode, and tolerances. It reports matched, missing, extra, and parent-mismatch rows; local and rest-world transform deltas; and child bone-length ratios where both corresponding parent/child rest-world positions are available. compatible means all required topology/rest facts were available and within the declared tolerances. incompatible names a complete mismatch. partial and not_evaluated record unavailable evidence and exit 1. Skin membership, inverse-bind evidence, and deformation-model evidence are separate facets; they never become an opaque score or turn this structural result into a deformation claim.

report --compare-after writes one offline synchronized before/after diagnostic. Both input files and both clip names are explicit; it refuses duplicate or incompatible named skeletons, missing/ambiguous clips, unavailable metric grids, identical or incomplete rooted dependency closures, and report work beyond its fixed bounds. Authored duplicate take names remain ambiguous even when a loader gives normalized clips distinct display suffixes. Each panel shows both its primary input and complete closure identity, and includes only selected-clip or whole-input gaps and predictions. The output cannot alias either primary input, a rooted sidecar, or the selected config; refusal preserves any existing output. The two panels synchronize by normalized sampled-frame phase and display both source times — that display mapping is not an authored time warp. Findings, coverage gaps, prediction provenance, and identities remain separate per side. Selecting a finding scrubs the linked time and highlights its uniquely mapped bone. One toggle beside the shared phase draws the two judged poses in a single pane instead of two — the before skeleton solid, the after one dashed over it, at the same two selected frames and through the same shared camera — and the two-pane layout stays the default. The overlay stands down while a loop-seam context is selected, because that context is one side's own two endpoint frames rather than one shared phase. The report is evidence for troubleshooting, not an artistic, gameplay, engine, or retargeting acceptance verdict.

--evidence-only omits the sampled pose grid from either report form and sets "evidence_only": true in the embedded data. The grid is the motion — every bone's model-space position on every judged frame — so a full report of a licensed clip carries that clip. Both forms keep their findings, coverage gaps and engine predictions; the comparison also keeps both sides' input identities, and the single-clip report its profile and source path (when the document has one). Both show a notice where each pose view would be and disable playback; the comparison also disables its pose overlay, which draws the same grid. So the document can be attached to an issue, published, or sent to a vendor where the source asset itself may not go (see the licensed-asset policy). Nothing else about the run changes: the same checks evaluate the same frames.

What is left to look at differs by form, because only one of them draws its charts on the Rust side. The single-clip report keeps its charts, and they retain exactly the root's X/Z path, the two foot-height series relative to the hips plus their difference, the last on its own right-hand axis, and — for each declared gait group with a member in the file — one left-minus-right foot-height series per member with its measured stride anchor. Nothing in any of them is per bone.

--clip NAME and the gait-group figure meet deliberately. The clip filter scopes the document to one clip: its pose grid, its findings and its own two charts. A gait group is a contract between clips, so the figure for a group that clip belongs to keeps drawing and naming every declared member — a group compared against one of its own members is not a comparison. Groups the selected clip does not belong to are not drawn at all. What that publishes beyond the selected clip is one left-minus-right foot-height series and one measured anchor per member, which is the single-series metric evidence the licensed-asset policy allows; the pose grid of a clip the filter excluded is never embedded. Every comparison panel is a viewer drawing made from the pose grid — both role-trajectory panels, both gait panels, and the shared root chart — so an evidence-only comparison replaces all of them with the same notice. Everything else the full comparison embeds stays: findings, coverage gaps, prediction provenance, identities, clip metadata, and the contexts each side was judged in. Rendering those panels on the Rust side is tracked as follow-up work and would restore them.

Both report forms follow the reader's prefers-color-scheme and read a URL fragment of &-separated key=value options, so one generated file can be deep-linked or embedded without regenerating it. Both honour theme=light and theme=dark, which pin the palette; embed=1, whose rules hide the running title and the interaction hint and nothing else, so the document fits an <iframe> with its findings, charts, and evidence disclosures in place; and frame=N, which scrubs to a judged frame. clip=NAME, with=NAME, and finding=INDEX are single-clip options — they land where clicking that clip, that pairing, or that finding lands, on the same selected row, frame, chart playhead, and 3D view — while the comparison addresses a finding through the #finding-<side>-<anchor> link its own panels carry, because its clip correspondence is declared by the two inputs rather than chosen in the viewer.

with=NAME names a second clip of the same document to play beside the selected one, which the viewer's with control also chooses. The two poses occupy the two halves of the one canvas, drawn through a single camera fitted to both clips' bounds so they share a scale, and coloured by the report's left/right tokens; a key below the canvas names each half in the colour that half is drawn in, and both clips' charts are shown.

The transport drives the selected clip, and each half is shown at the frame nearest the transport's normalized phase in its own grid, with both source times labelled. That one frame is also where that clip's chart playhead and root-path dot sit, so a chart never reports a frame other than the one its pose is drawn at. This source-phase mapping selects samples the checks already judged; it does not retime or resample authored clip time. An empty value, a missing or ambiguous name, and the selected clip's own name clear the pair. An evidence-only report has no sampled positions or local transforms to draw, so the omission notice replaces playback and the pairing selects charts only.

With two clips selected, enable illustrative blend to add a third pane. The weight slider runs from the primary clip at 0 to the paired clip at 1, starting at 0.5. The pane blends their sampled local transforms and runs forward kinematics; it does not interpolate model-space joint positions. It is labelled engine-agnostic illustrative blend, not Bevy, Unity, Unreal or Godot runtime evidence. Both source frames/times and the weight remain visible. The phase still chooses the nearest frame independently in each grid, so unequal durations are paired by phase rather than source seconds.

All panes share the source-based camera and existing orbit/zoom controls. Interior rotations can extend beyond the initial source bounds; zoom out if needed. Oversized added local data or malformed local samples show a blending omission reason without removing source findings or playback. The HTML blend contract defines the math, limits and motion-data boundary.

A key that is absent leaves that state as it is, so following one of those anchors inside an embedded, theme-pinned report does not un-pin it. A key that is present is applied as far as the document allows: a syntactically valid frame beyond the clip is clamped to its last frame, and a clip the document does not contain selects the first one, because a reader who asks for a position wants the nearest one the report can show. A value the parser cannot read at all restores that state's default instead — an unparsable frame restores frame 0, an unparsable clip the first clip, and a finding index the document cannot address clears the selection. Unknown keys and malformed pairs are ignored, no option changes the findings, coverage, or charts the document carries, and no viewer writes the fragment back.

The checked-in synthetic comparison walkthrough demonstrates seam endpoints, sampled stance and foot trajectories, shared-scale root paths, and visually unchanged structural-track remediation. Its generated GLBs are byte-checked against the repository-owned fixture builder in CI.

--config animsmith.toml is global for document-local commands. Without it, the CLI auto-loads ./animsmith.toml when present and otherwise uses built-in defaults. Collection commands deliberately reject the global spelling. collection lint uses each source config declared in the collection manifest, or exact built-in defaults when none is declared; it never discovers an ambient ./animsmith.toml. collection evaluate-directional-speed has no config or output-path option: its policy and evidence inputs fully declare its boundary.

collection dashboard is an offline current-state view, not a second collection ledger. It accepts only the strict current collection-output V11 contract, writes a self-contained HTML file and the separately versioned urn:animsmith:schema:collection-dashboard:1 authority that binds the exact input bytes, and never reloads source assets. Its optional transition-pose evaluation must bind the same manifest bytes. --asset-report names an exact logical clip and a safe relative HTML reference; the dashboard links it but never embeds source assets or samples. Its role filter means already-recorded resolved skeletal roles, not inferred gameplay or clip semantics. Filtering only hides rows locally: it cannot turn partial, unavailable, excluded, or incomplete evidence into a complete collection. The separate declared-source inventory remains visible even for sources with zero logical clips and retains observed identity plus availability, loader, and dependency-closure states. Every observed physical take remains source-owned and records its source take index/name, normalized index/name state, evidence, and outcome even when no logical clip declares it. Global finding/gap/prediction totals derive from those physical rows plus unscoped source findings and unscoped source-level required_prediction_unavailable facets; logical rows are navigation projections and are not counted a second time. Source/document findings without an exact clip match remain explicit unscoped source findings and still contribute to the unfiltered finding total; they are never guessed onto a clip row. Subjectless required-unavailable prediction facets likewise remain explicit source-level dashboard evidence with their recorded reasons; the dashboard does not invent a take or logical clip witness for them. When one source contains the same normalized name at multiple physical indices, name-addressed nested evidence has no unique witness. The dashboard retains each finding once as unscoped source evidence and marks the affected physical rows duplicate_normalized_clip_name; it does not copy findings, gaps, or predictions onto either take or logical row. Optional transition members carry a logical clip only when their source-input identity plus take index/name uniquely match one dashboard declaration. Strict readback rederives that relationship and rejects missing, forged, or ambiguous logical resolution. Every logical row whose availability is established must also reconcile its source, source-take index/name, evidence, coverage, and outcome with exactly one source-owned established physical row. Runtime-set gaps and lifecycle are also rederived from member availability: each non-established member requires one member_unavailable gap in member order, and complete is valid only when that derived gap list is empty. Strict readback rejects contradictions.

evaluate-transition-poses is a JSON-only, single-result transition-family contract, not a lint/check stream. It evaluates exact named/indexed takes in the input against document-local [transition_families."<id>"] declarations and never changes the input. When no config file is selected or found, its declaration authority is the exact zero-byte TOML input; this is deliberately the same authority as an explicitly empty config file. An absent or empty transition-family table emits no_configured_families and exits 0. With families, all complete passes exit 0; findings or any incomplete family emit the immutable result and exit 1. Invalid config/declaration, input loading, or output errors emit no result and exit 2. Collection transition-family reload/evaluation is a separate command boundary.

transform --gait-anchor is an explicit declaration that each selected clip is an in-place cyclic gait. Before rewriting, it samples the configured Root role (or Hips fallback) and refuses the whole command if trajectory evidence is missing/non-finite, horizontal accumulation exceeds 1 cm, or yaw accumulation exceeds 1°. Every nonconstant channel the operation would rotate must contain exactly one key at each --fps whole-frame sample over the clip duration, at the exact representable f32 key / fps time and period endpoint. Sparse, differently framed, duplicate-time, or off-grid trajectory evidence refuses, as do duplicate (bone, property) channels (including constant channels), so the phase shift remains a bijective authored-value permutation. Verification samples the exact admitted f32 key times, and mutation permutes output values by integer key index rather than resampling. Constant channels are exempt and cannot influence the declared period or shift. Before sampling, the command validates the complete skeleton, resolved metric roles, track targets/cardinalities, and finite evidence. Both declared-frame and maximum-authored-key work must independently fit these inclusive 1,000,000-sample bounds: declared frames × skeleton bones, declared frames × tracks, and maximum authored keys × skeleton bones. The translation and yaw caps apply directly; no interior step is an allowance. Yaw uses f64 first/final headings plus counted full-turn crossings. At sample zero it selects the local +Z, +Y, or +X axis with the greatest finite horizontal projection, in that tie order, and keeps that axis for the complete proof. A different source-axis convention is therefore accepted without per-sample axis switching; if the selected axis later has no horizontal projection, the command refuses. This avoids error growth with the admitted segment count. Four f32 successors at each inclusive cap cover only authored endpoint translation/quaternion quantization; other checks are unchanged. A refusal names the clip and selected bone, does not publish or replace the requested output, and emits no earlier per-clip success lines: standalone transform stdout is buffered until all selected clips and the artifact write succeed. Keep authored root motion unchanged, apply a runtime phase offset, or use separately designed trajectory-preserving tooling. The option does not convert root motion to in-place motion.

scale is the atomic linear-scale producer. It has two distinct subcommands because the two operations rewrite different domains and a factor alone does not identify which one was meant: whole-document converts every represented length by a declared factor (physical size changes; use it only when the source was authored in a different linear unit), and rest-bind removes one compensating inherited scale from the skinned hierarchy anchored at a declared source skin and source root node, preserving world joint translations and orientations, sampled trajectories, and skinned vertex positions while removing the composed scale. The scale workflow owns the operation-choice, glTF raw rewrite or narrow FBX staging, proof, publication, and support-boundary walkthrough; this page remains the installed command/flag/exit reference.

Every numeric and source-identity argument is required. Nothing is inferred from mesh bounds, character height, joint lengths, inverse-bind magnitude, filename, or asset category; there is no implicit first skin or root, no animsmith.toml key, no plan file, no in-place mode, and no per-run tolerance flag — the tolerance policy is fixed and its identity is recorded in the evidence. Input, output, and evidence paths must all be distinct. glTF/GLB keeps its input container; the narrow FBX rest-bind path emits a new .glb.

Accepted inputs are self-contained glTF/GLB for both operations, plus a complete-inventory .fbx for narrow rest-bind when the default FBX feature is enabled. See the scale workflow for the complete support boundary and rewrite/reload/proof sequence, and the output reference for typed refusal records and deterministic publication details.

scale splits its two failure codes by what the failure is a property of, not by how far the run got. A refusal that is a property of the input asset publishes nothing, leaves any prior pair byte-identical, and exits 1 — with the typed reason as prose on stderr under --format text and as the same evidence record with outcome: "rejected" on stdout under --format json. That includes bytes that do not parse as the glTF/GLB the extension declares, and a document whose size puts the sampled proof over the policy's work budget. A failure that is a property of the invocation or of the operator's filesystem exits 2 with prose on stderr: a declared factor that is not finite and positive, an input that cannot be opened, a wrong extension, a container the extension disagrees with, two arguments naming one file, or a missing output directory.

The three paths must name three different files, and each is compared by the file it actually reaches: the input is resolved through a symbolic link, because reading follows one, while a destination is not, because publishing renames over a link rather than through it. Passing a symlinked input alongside a destination naming its target is refused before anything is written. A symlinked input that aliases neither destination is accepted.

transform --drop-duplicate-loop-endpoint is the narrow mechanical transform for an inclusive DCC cycle export that copied frame 0 to the final frame. It accepts only a strict authored-key candidate: all channels must have one common finite, strictly increasing timeline; valid key cardinality; matching first and last values (vector components within 1e-5, sign-invariant quaternion angle within 1e-4 radians); and actual interior motion. It atomically removes the same complete terminal key count from every channel (including each cubic-spline key triplet), leaves retained data unchanged, and re-pins duration to the last retained key. Other clips are not generalized, retimed, or repaired by this flag. The resulting open-cycle representation no longer satisfies inclusive loop-closure, which expects a repeated final sample; the complete endpoint-mode classifier remains #22.

transform --prune-constant-tracks is an opt-in companion to the default constant-track note. It removes only multi-key translation, rotation, and scale tracks whose evaluated keyed values are constant: vector components may vary by at most 1e-4, and rotations by at most 1e-3 radians under sign-invariant quaternion comparison. Single-key pins, malformed/non-finite data, and cubic-spline tangents that create motion above tolerance are not candidates and remain unchanged. For each candidate, the transform either removes it or prints a refusal reason: removing it would change sampled local TRS or model-space position/rotation, it is the clip's last remaining track, sampling is unsafe, or it targets a bone named by that clip's effective animates_bones expectation. The latter preserves the evidence needed for missing-bones and frozen-bone; [rig] required_bones does not protect tracks because it is a skeleton-presence contract.

Pruning runs after any selected slice, hold extension, duplicate-endpoint removal, and gait anchoring, so the final clip is what is judged. Text output lists every removed or retained candidate with its original track index, bone, property, interpolation, key count, and (for retained tracks) reason; no machine-readable output schema changes. It does not reduce changing keys, rewrite DCC curves, remove custom curves AnimSmith does not model, decide whether a non-rest constant pin is artistically intentional, or repair cubic tangents.

Exit Codes

CodeMeaning
0No failing findings and no required-unavailable engine prediction facets; warnings, notes, or ordinary coverage gaps may remain.
1At least one failing finding, any required_prediction_unavailable facet (including an embedded Bevy addressability evaluation), an incomplete collection lint result, an incomplete or not-evaluable directional-speed or transition-pose evaluation, a significant diff, pending repairs under fix --dry-run, or a scale, convert, or assemble refusal that is a property of source asset bytes.
2Operator/tool error: unopenable input, bad config, unsupported format, or invalid flags.

The code reports what the run did, never how well it could report it. This holds for parser-rendered help/version, text, Markdown, and every --format json path (measure, lint, evaluate-transition-poses, collection evaluate-transition-poses, collection lint, collection evaluate-directional-speed, diff, convert, assemble, scale, generate addressability). If stdout cannot accept the result — a closed pipe or full filesystem — the checked write never panics, a best-effort checked diagnostic goes to stderr, and the stdout-bearing path's already-established code stands except for evaluate-transition-poses or collection evaluate-transition-poses. These immutable results have no sidecar or previously established outcome: a failed stdout write is an operator error, produces no usable result, and exits 2. Thus lint … --format text | head still exits 1 for findings it found, inspect … | head still exits 0 for an inspection it completed, and scale still exits 1 for a refusal or 0 for a published pair. Stderr may itself be closed; losing both streams is still not a panic. Raising the stdout failure instead would report an operator error for work that was actually done and make exit semantics depend on presentation format. JSON serialization failure remains exit 2 because the CLI could not form a truthful record; delivery failure after rendering is only reporting for the ordinary streams. Other operator errors occur before stdout reporting, remain stderr-only, and retain exit 2.

For artifact producers, classification is by typed provenance, never by diagnostic wording. The JSON refusal identity is urn:animsmith:schema:producer-refusal:1:

ClassExamplesCode and streams
Asset-property refusalSource bytes do not parse or are structurally unsupported; a valid recipe names a missing/ambiguous take, node, mesh, or material; static bake, canonicalization, gait, scale, proof, or output representability rejects the asset.1; JSON writes one producer-refusal:1 record to stdout and nothing to stderr, while text writes one escaped typed refusal to stderr and nothing to stdout.
Operator errorInvalid flags/config; recipe syntax, schema, or intrinsic values; unsupported extension; missing/unreadable declared file; unsafe, aliased, or unwritable paths; temporary/write/publication/rollback I/O; refusal-record serialization.2; prose on stderr and nothing on stdout in either format.

A refusal is established before publication. It leaves any prior assembly artifact/evidence pair and any prior convert artifact byte-identical; the foot-cycle producer never replaces a generation. Convert still writes its successful single artifact directly, so this is not a rollback promise for an operator I/O failure during that write. Commands that render several related pieces attempt one checked stream when a single delivery boundary is promised. In particular, all selected fix repair reports and all parts of one conversion summary produce at most one stdout-failure diagnostic. Help and version delivery uses clap's own fallible styled writer, so forced ANSI color remains intact while a closed destination still follows this rule. Only stdout is affected; nothing about artifact publication changes.

A role-dependent check with missing prerequisites reports a typed coverage gap and does not fail the run — exit 0 means no failing findings among the checks that evaluated, not that every declared check evaluated; see reading a lint run for the full outcome vocabulary. A required-unavailable engine prediction is distinct: it exits 1 and cannot be suppressed by severity or --allow.

Use lint --deny-warnings when CI should fail on warnings as well as errors. fix --dry-run is the repair check mode: it exits 1 when the file has repairable defects and 0 otherwise, so CI can gate on "this asset needs fixing" without writing anything. The exit code reflects repairs fix would actually perform: tracks it cannot patch (data-URI buffers, cubic quat-norm tracks, quantized rotations) are printed as skipped[...] but do not fail the check — gate on lint (the quat-norm or quat-flip checks) when detection alone should fail CI.

Unsupported glTF Encodings

AnimSmith reads a narrower slice of glTF than the specification permits. A mesh accessor it cannot decode exactly as declared is refused with exit 2 rather than read as something else, so a run never reports numbers derived from bytes it misread.

Integer TEXCOORD_0 and WEIGHTS_0 accessors are accepted only when they declare "normalized": true, as glTF requires. The upstream reader rescales UNSIGNED_BYTE and UNSIGNED_SHORT values even when that flag is missing; AnimSmith refuses the unflagged accessor before decoding so measure cannot present an unauthorized rescaling as authored data. Normalized integer and FLOAT forms retain their decoded values. Re-export the malformed attribute with the flag, or as FLOAT; the refusal identifies its mesh, primitive, semantic, and accessor.

The common case is KHR_mesh_quantization: gltfpack and similar mesh optimizers store POSITION and NORMAL as BYTE/SHORT instead of FLOAT, and AnimSmith has no decoder for those. Which message you get depends on how the file declares the extension:

$ animsmith lint quantized.glb
animsmith: glTF parse error: invalid glTF: extensionsRequired[0] = "KHR_mesh_quantization": Unsupported extension;

$ animsmith lint quantized-declared-used-only.glb
animsmith: mesh 0 primitive 0 POSITION: accessor 0 is VEC3 of SHORT, but the loader reads VEC3 of FLOAT

Re-export without quantization — gltfpack -noq, or turn off the quantization option in whichever exporter produced the file — and the same asset loads. AnimSmith measures rigs and animation, so the unquantized source is the right input for it; keep the quantized build for shipping.

Two other shapes are refused with the same mesh N primitive M message: an accessor typed for a different element than the slot that references it (a VEC3 TEXCOORD_0), and one whose buffer layout the reader cannot walk (a byteStride shorter than its own element, a sparse block of count 0, or a dense/sparse byte extent beyond its declared buffer view or the external, data-URI, or GLB bytes that actually resolved). Short extents are refused rather than silently loaded as empty positions or indices, so a clean measurement never substitutes absent geometry for unreadable authored values. Those are invalid glTF rather than an unsupported feature, and the fix belongs at the source. The Khronos glTF-Validator names the same accessor — ACCESSOR_SMALL_BYTESTRIDE for the stride, and the schema's minimum: 1 on sparse.count for the sparse block — so run it on the file to see the defect in its own vocabulary. AnimSmith still has to check these itself, because the gltf crate it parses with does not: that crate's JSON validation bounds byteStride to 4..=252 without ever relating it to the accessor's element, so an unwalkable layout passes parsing and reaches the reader.

The same layout defect on an animation sampler is refused too, naming the clip and channel instead of a mesh:

$ animsmith lint poisoned-track.gltf
animsmith: clip 'walk' node 3 sampler input: accessor 0 reads its elements from buffer view 0, whose byteOffset 18446744073709551615 plus byteLength 12 is a byte extent that overflows

Sampler element encodings are checked independently before the selected reader is constructed. Inputs must be SCALAR of FLOAT; translation and scale outputs must be VEC3 of FLOAT; rotation outputs must be VEC4 and may use any of glTF's five decodable quaternion component types (BYTE, UNSIGNED_BYTE, SHORT, UNSIGNED_SHORT, or FLOAT); morph-weight output uses those same five component types as SCALAR. A mismatched type or component type is an operator error rather than a panic or a size-coincident reinterpretation:

$ animsmith lint mistyped-track.gltf
animsmith: animation 0 sampler 0 input for node 0 translation: accessor 0 is VEC3 of FLOAT, but the loader reads SCALAR of FLOAT

Feature Flags

The default binary enables fbx and report.

cargo install animsmith
cargo install animsmith --no-default-features

The no-default-features build has no C toolchain dependency and keeps the glTF-only workflow: inspect, measure, lint, transform, fix, scale, collection transform-foot-cycle, and diff. scale and the collection foot-cycle command are the minimal build's evidence-emitting producers. The HTML report command is controlled by the report feature. convert accepts FBX or glTF input (a glTF input is re-emitted, carrying its geometry) but is compiled only with the fbx feature. assemble is gated by the same feature because its maintained boundary accepts FBX sources as well as glTF.

The exact foot-cycle producer form is:

animsmith collection transform-foot-cycle COLLECTION.toml \
  --parameterization FOOT-CYCLE.toml --format json

--format json is required and JSON is the only format. There is no --output or --output-dir: the strict parameterization document is the sole destination authority. The destination must not exist. Success publishes one complete generation with exactly three files per declared member plus aggregate-evidence.json, then writes the exact aggregate file bytes to stdout. Valid asset/capability/refinement/proof refusal exits 1 with one producer-refusal:1 JSON record. Malformed or stale controls, unsafe paths, I/O, serialization, and publication races exit 2 with empty stdout. Full-scene conversion carries factor-only materials plus linked or embedded PNG/JPEG base-color, normal, metallic-roughness, and occlusion textures. Normal textures retain their glTF scale; FBX normal maps use glTF's default scale because ordinary FBX materials do not expose the same scalar. See the static asset workflow guide for why the normal slot matters, common bad handoffs, and the engine-side checks that conversion cannot perform.

--material-texture-recipe <PATH> applies explicit BaseColor, normal, metallic-roughness, and occlusion image mappings during conversion. It conflicts with --animation-only, which removes materials, and is compatible with --bake-static-mesh-transforms and either --format value. Without a recipe, ordinary linked and embedded textures keep their existing conversion path. See material texture recipes for the recipe contract, deterministic processing policy, and path rules.

assemble reads a versioned multi-source character recipe, uses one input as the authoritative skinned base, and exact-name remaps selected takes from other FBX or glTF inputs. It writes a GLB and assembly-evidence JSON as a rollback-safe publication pair. The command owns generic asset transforms; source extraction, project policy, and publication remain consumer concerns. The current recipe/evidence pair is v7. Recipe v7 may include an optional [rest_bind_scale] block whose exact root_node_name and expected factor are both required. The base must resolve that name to exactly one source node and exactly one non-empty source skin whose every joint is that node or its descendant. Every distinct FBX clip input uses an animation-only projection. A glTF/GLB clip retains its existing successful full rest/bind or meshless track-only path; role-specific admission additionally selects the track-only path when only unused geometry, deformation, material, or bind obligations would fail. Framing, dependency, raw-coverage, named-skeleton, and animation accessor/layout obligations remain strict. The compatibility domain contains the selected base skin joints, the root, their named ancestry, and each actual track target with its ancestry. Unreferenced base-only geometry or attachment descendants are not required in the clip. Boundary whitespace is invalid rather than trimmed. The block accepts glTF/GLB plus the narrow normalized/baked FBX subset admitted by the existing rest-bind capability boundary. That boundary may admit user-defined FBX properties and bounded external texture/video declarations after same-load evidence proves they are not scale-bearing. It also admits exactly ufbx's marker, LOD-group, stereo-camera, camera-switcher, and display-layer typed lists because they cannot supply hierarchy transforms, skin binds, tracks, or geometry to the normalized rest/bind bridge. Display layers contribute only node membership and editor visibility/freeze/color state. The boundary admits shader/binding-table metadata on the same basis. BindPose rows are admitted only when they cover every joint of each skin they touch and are finite, unambiguous, and reconcile with the converted cluster bind or node rest-world matrices already consumed by the bridge; no Pose remains required. Those rows remain counted in the raw aggregate; every other unmodeled typed list stays fail-closed, with exact nonzero kind counts in the refusal. The boundary still may admit enumerated scale-invariant conversion fidelity—omitted authored vertex/face/edge metadata, influence projection with complete effective coverage, triangulation, and exact-bit welding—while retaining the public inventory evidence. It refuses incomplete resource or construct coverage, extensions, and every unsupported transform, geometry, bind, or animation fact. A refusal names the exact failed fact or counter. An artifact or evidence destination that names one of the safe source-relative dependency keys retained by the same-load closure is an operator error even when capture was unavailable or refused; symlink-mediated dependency keys stop before publication rather than resolving through the link, and resource-budget truncation stops rather than trusting an unchecked tail. Publication never replaces a source sidecar. The command validates the base plus every clip's versioned skeleton basis before remapping any keys. V7 retains v6's composition with canonicalize_skin, ground_and_center, and remove_nodes: it applies those normalized assembly transforms to both the staged source and the rebased clip reference, then performs one final raw staged-GLB rewrite and proof. Recipe v4 retains its released refusal for that combination. Set prune_constant_tracks = true to remove only tracks proven constant after all other transforms, including track completion. Because this is all-property pruning, it can remove completion-generated (bone, property) coverage whose completed value is constant. Effective output clip animates_bones exact names are retained, while required_bones is never used as the carve-out. Evidence records every removed track and is empty when pruning is disabled or removes nothing. Leave pruning disabled where consumers need dense transition coverage and do not explicitly reset omitted properties; property-scoped selection is tracked in #401. Root-level remove_nodes exact-names base nodes and removes their descendant closure after animation transforms; any surviving track, skinned mesh-instance, or skin reference refuses the operation. With FBX rest_bind_scale, an unskinned mesh instance attached inside that declared closure is excluded from the private scale stage and removed with the node. It still blocks the scale plan when the node is not declared for removal. For every track-only clip input, v7 applies the accepted base plan's named animation-target factors (including cubic translation tangents) through the animation-only projection without inventing a bind rewrite. The named root and selected base skin-joint basis must still match, as must every actual track target and the named ancestry connecting those nodes. Unreferenced base-only geometry descendants do not participate in that clip proof. The projection never applies to the base; every base and full rest/bind path retains the strict raw capability contract. It performs no material, texture, or mesh garbage collection. Recipe/evidence v1 through v6 remain immutable historical contracts; v3 rejects rest_bind_scale as unknown. The recipe identity is urn:animsmith:schema:character-assembly-recipe:7; see character-assembly-recipe-v7.schema.json.

Static mesh transform bake

convert --bake-static-mesh-transforms is an explicit, opt-in conversion operation for a static mesh whose normalized placement must live directly in mesh-local geometry. It accumulates each accepted mesh node's rest transform through its hierarchy into positions, transforms normals with the inverse-transpose and normalizes them, and writes the result beneath a canonical identity root. Indices, UVs, model-supported material assignments, and embedded base-color, normal, metallic-roughness, and occlusion textures are retained. The default conversion is unchanged; --bake-static-mesh-transforms conflicts with --animation-only.

The operation fails with exit code 2 rather than guessing when the input has any animation track, a skin signal, a mesh definition with no unambiguous single instance (including shared definitions), malformed or non-finite scene data, a singular or near-singular transform, or a reflection. Skin baking, animated-node baking, and reflection handling are outside this operation's contract. Same-platform runs over the same input and options produce a byte-identical artifact. The static asset workflow guide shows the before/after model, when baking is appropriate, and why this is not a general scene flattener.

Repairs

Every repair is safe, lossless, and idempotent — that is the bar for adding one. Repairs have stable ids so scripts can pin exact behavior:

Repair idBehavior
quat-normUnit-normalizes finite, non-zero LINEAR/STEP quaternion keys. This is lossless because scaling a quaternion does not change the represented rotation after normalization. CUBICSPLINE tracks are skipped to preserve tangents.
quat-flipNormalizes adjacent quaternion keys to the same hemisphere. This is lossless because q and -q represent the same rotation.

By default fix runs every repair. --repair id[,id] pins an exact list (animsmith fix --help names the valid ids). fix writes only when you explicitly choose a destination; --dry-run reports and sets the exit code without writing:

animsmith fix clip.glb --dry-run
animsmith fix clip.glb -o fixed.glb
animsmith fix clip.glb --in-place
animsmith fix clip.glb --repair quat-norm,quat-flip -o fixed.glb

Engine profiles and importer settings

An optional [engine] section selects one exact, versioned importer contract. There is no generic, automatic, nearest-version, or fallback profile. The released profile tuples are:

profilerevisionengine versionimporteraccepted source
unity-generic26000.3fbx-model-importerFBX
unity-humanoid16000.3fbx-model-importerFBX
unreal15.8fbx-importerFBX
godot14.7resource-importer-sceneglTF, GLB, or FBX
bevy10.19.0gltf-asset-loaderglTF or GLB
bevy20.19.0gltf-asset-loaderglTF or GLB
bevy30.19.0gltf-asset-loaderglTF or GLB

The accepted-source column is AnimSmith's bounded profile boundary, not a claim that the named engine supports no other source formats. An absent [engine] keeps the existing engine-neutral behavior.

Unity Generic revision 2 exposes the current closed setting vocabulary. Every applicable setting is required because the cited Unity 6000.3 documentation does not establish a default that AnimSmith can safely materialize:

[engine]
profile = "unity-generic"
profile_revision = 2
engine_version = "6000.3"
importer = "fbx-model-importer"

[engine.settings]
animation_type = "generic"
avatar_setup = "create_from_this_model"
import_animation = true
root_motion_source = "Reference/Root"

[clips."locomotion_*".engine_settings]
root_rotation = "extract"
root_position_y = "bake"
root_position_xz = "extract"

[clips.idle.engine_settings]
root_rotation = "bake"
root_position_y = "bake"
root_position_xz = "bake"

root_motion_source is an exact document-scoped source-transform path for Unity Generic revision 2. It is not applicable to Unity Humanoid. The three bake | extract choices are clip-scoped and resolve with the normal field-by-field rule: matching globs in lexical order, then an exact clip name last. Every real Generic clip must resolve all three choices. The frozen revision-2 document controls are animation_type = "generic", avatar_setup = "create_from_this_model", and import_animation = true. convert_units and bake_axis_conversion belong to the preserved revision-1 advice vocabulary, not this root-motion profile. Unreal, Godot and Bevy revision 1 expose no V1 settings, so supplying any setting is an error. Bevy revision 2 is the exact profile used by engine-unit-scale. It requires the load-time extension-handler environment and animation-feature state; other loader settings have version-pinned defaults that may be overridden explicitly:

[engine]
profile = "bevy"
profile_revision = 2
engine_version = "0.19.0"
importer = "gltf-asset-loader"

[engine.settings]
extension_handler_environment = "bare_empty" # or "bevy_pbr_stock_0_19"
bevy_animation_feature = true
rotate_scene_entity = false
rotate_meshes = false
load_meshes = "nonempty" # or "empty"
load_animations = true

The two handler values are closed, version-pinned environments: an empty registry, or exactly Bevy 0.19's stock PBR handler. Project/user extension handlers are outside the profile. The revision-2 settings are document-scoped; clip engine_settings are rejected.

The current revision 3 keeps the revision-2 tuple and settings and adds only the engine-track-support animation/channel gate. That check consumes bevy_animation_feature and load_animations; both retain whether their values were explicit or defaulted. The compiled feature gate has precedence: if it is disabled, the prediction is a dropped negative outcome regardless of the load_animations value. This revision uses load_animations to model the stock loader's animation path, not extensions, other animation constructs, or successful runtime survival.

The rule binds a bounded same-load raw animation inventory and independent per-animation channel coverage. A complete empty inventory is not applicable. Partial or unavailable source inventory emits exactly one subjectless, unsuppressible required_prediction_unavailable inventory facet and no retained-prefix predictions. A complete inventory may emit one negative facet per affected animation or channel row when a gate drops it. If complete source evidence demands more than the shared facet budget, the allocator-authorized prefix remains available and one canonical facet_budget_exceeded summary records the omitted suffix; this does not relabel the source inventory as partial. When both gates allow loading, the required result is instead a stable required_prediction_unavailable runtime-survival state. Dropped rows are not content findings and cannot be suppressed with --allow; any required unavailable facet still exits 1.

Revision 3's historical artifacts use output-v16 and V5 prediction provenance/readback. Revision-2/V4 and output-v15 readers and historical examples remain preserved. Unity Generic root-motion uses the separate V6 provenance and current output-v19 contract; output-v9 and every later historical reader remain immutable and readable.

All statically knowable tuple, setting, value, scope, and applicability errors are reported before input I/O. Accepted input format and required per-clip materialization are checked after loading. Profile selection never changes measure values. Current output-v19 records resolved profile provenance on lint files.

The first production rule is engine-addressability for the exact Bevy revision 1 / 0.19.0 / gltf-asset-loader tuple. With complete glTF/GLB source animation inventory, it emits one available facet whose subject is the exact Bevy display label Animation{i} for each source animation index i:

animsmith --config examples/bevy.animsmith.toml lint \
  --select engine-addressability examples/assets/walk.glb

Named, unnamed, and duplicate-named animations all use their distinct source indices; names do not become typed labels. Partial or unavailable source clip coverage emits one blocking animation_asset_label_inventory facet rather than predictions for the retained prefix. The rule predicts only the canonical GltfAssetLabel::Animation(i) selector spelling. It does not prove Bevy ran, that animation loading was enabled, that the runtime asset exists, or that its targets and graph wiring are usable. The selector can change when source animation order changes.

Current lint uses prediction provenance v3 with bounded 4,096/N+1 settings coverage. A 4,097th clip is retained as typed partial-settings overflow evidence, never as a complete prefix.

For the exact Unity Generic revision-2 tuple, engine-root-motion uses V6 provenance. It evaluates each explicitly declared movement_owner_xz, movement_owner_y, and movement_owner_yaw against the corresponding clip root_position_xz, root_position_y, and root_rotation setting. bake produces baked_into_pose, extract produces stored_as_root_motion; matching project/importer ownership is compatible, and a mismatch is an ordinary error finding with a scoped prediction facet. No numeric travel threshold is used: measured translation/yaw availability is required, while the measured magnitude is only evidence.

The path must match the explicitly resolved Root role in the raw FBX transform inventory. Hips is never a fallback for this engine rule. A missing or ambiguous path, Root mismatch, incomplete/unavailable raw inventory, incomplete intent/settings, duplicate clip name, or unavailable axis measurement emits required_prediction_unavailable; this is not a content finding, cannot be suppressed with --allow, and makes lint exit 1. Complete all-available work is complete, mixed work is partial, and all-unavailable work is not_evaluated. Raw path matching is case-sensitive and byte-exact; the only separator is unescaped /, with nonempty segments, no ./.., backslashes, controls, or Unicode format characters, and limits of 1,024 bytes per segment, 4,096 bytes per path, and 256 segments. The same-byte raw FBX projection retains source identities and parent chains, excludes the implicit root and generated helpers from matching, and reports NoMatch only under complete coverage. AnimSmith does not execute Unity, perform imported-asset readback or runtime playback, or certify engine behavior. The checked-in and CI fixtures for this slice are self-authored synthetic FBX only.

For the exact Unreal revision 1 / 5.8 / fbx-importer tuple, lint also runs engine-clip-boundary. The FBX adapter's same-load exact timing evidence is checked only for an absolute animation-stack end coordinate on its exact frame lattice. Missing or incomplete evidence is a required-unavailable facet; the command does not infer import ranges or other Unreal settings.

generate addressability emits the immutable V1 animation-only contract by default and whenever no exact Bevy revision-3 profile is selected. It emits canonical JSON by default:

animsmith generate addressability examples/assets/walk.glb \
  > walk.addressability.json

The neutral inventory retains source-order animation and channel indices, optional non-unique source names, channel targets and accessor indices, the primary-file identity, and the full dependency closure. Its identity covers only those neutral fields. Without an engine profile, or with a supported profile other than the historical exact Bevy revision-1 tuple above, the root bevy field is null. With that historical profile, it embeds same-load prediction provenance and the unchanged engine-addressability evaluation:

animsmith --config examples/bevy.animsmith.toml generate addressability \
  examples/assets/walk.glb --format markdown

Names remain metadata and never replace source indices. Partial or unavailable animation coverage stays visible in the neutral inventory. When the exact Bevy adapter is active, the existing required-unavailable facet makes the command exit 1; without that adapter the same neutral evidence exits 0. Non-glTF input, malformed or unknown profile selections, and an actual 4,097-clip input are operator errors (exit 2). Text and Markdown are escaped presentation views of the same validated value and add no conclusions to the JSON contract. This V1 does not inventory scenes, default scenes, skins, target paths or UUIDs, Bevy named-map winners, or extension support, and it does not certify a runtime load. Consumers using the strict staged reader must also keep each report at or below 256 MiB; the reader enforces this byte cap before UTF-8 or JSON decoding.

The richer exact-Bevy path emits the separate V2 contract urn:animsmith:schema:gltf-addressability:2, documented by gltf-addressability-v2.schema.json. It is selected only with the exact Bevy revision-3 profile/settings and keeps the V1 command contract and readers immutable. The same one engine-addressability evaluation is reused; rich rows do not introduce a second check lifecycle. Required-unavailable rich facets retain exit 1, complete or neutral output exits 0, and malformed tuple/config/input errors exit 2.

V2's raw inventory independently covers scenes, nodes, skins, node-to-skin attachments, and scene-root path candidates. It retains source indices, names, parents/children, ordered roots/joints, explicit skin.skeleton, and exact same-load primary/dependency-closure identity. Complete empty coverage proves absence; partial prefixes and unavailable domains do not. The explicit skin.skeleton member is source evidence only: Bevy ignores it and V2 makes no inferred-root claim. Scene-instantiated SkinnedMesh attachment is outside this static projection.

The pinned Bevy authority is tag v0.19.0, commit c6f634ca9f406d68ba5109d921247b654cb42c10, bevy_gltf 0.19.0 with locked gltf 1.4.1, plus commit-pinned label, loader, node-path, AnimationTargetId, feature, and root Cargo.lock sources. The isolated readback probe's committed lock is the graph authority: every Bevy 0.19 release crate in that lock is 0.19.0, and the probe rejects internal Bevy patch drift; independently-versioned helpers retain their own versions. Scene{i} exists for each declared scene; Gltf.default_scene routes only to an existing scene handle, with no DefaultScene label and no fabricated Scene0. Bevy eagerly creates Skin{i}/InverseBindMatrices for every source skin, including unreferenced skins, using identity fallback when inverse-bind data is absent. Skin{i} is created when any source node references it during the all-source-node pass; source skin indices, never collected-vector position, are identity.

Named scene/animation/skin maps are separate from typed labels and use source-order last-write-wins (skin winners follow lazily created skins in first-reference order). Target rows are per unique source animation target node and retain contributing animation/channel identities. Paths use authored names or GltfNode{source_index}, exclude the scene world-root name, and publish a path/UUID only when hierarchy, reachability, closure, feature settings, and collision evidence are complete. Missing bevy_animation, disabled load_animations, unreachable or multiply reachable targets, duplicate paths, UUID collisions, missing pointer width, and incomplete evidence are typed required-unavailable states. Target UUID reproduction requires explicitly declared 32- or 64-bit pointer width because Bevy hashes segment lengths using the target pointer width's little-endian encoding; host width is never inferred.

Each new rich V2 projection domain is bounded at 4,096 rows, with aggregate projection structural references at 65,536 and dynamic projection text at 1 MiB. The sealed embedded V1 animation inventory and CheckEvaluation scopes retain their own bounds. Names/path segments are capped at 1,024 UTF-8 bytes, paths at 4,096 bytes and 256 segments, and staged reports at 256 MiB. The explicit target_coverage projection reports target_domain_truncated at its row ceiling and is also required-unavailable whenever raw or animation/channel target-domain evidence is incomplete; other new-projection budget exhaustion reports projection_bounds_exceeded. No retained prefix is treated as collision-free. Strict readback rejects N+1 collections and contradictory states. A bounded second parse of already captured primary bytes is allowed within the same loader invocation; reopening the input or dependencies is not. V2 is prediction evidence only and does not claim runtime load success, target survival, scene spawning, graph wiring, or playback.

When the one shared check would exceed core's per-file facet budget, its rich prediction is compacted to one subjectless engine-addressability:facet-budget scope with facet_budget_exceeded. This preserves the existing one-lifecycle contract and does not turn omitted facets into positive evidence.

generate contact-fragment publishes canonical contact-fragment:1 bytes to --output. It selects one exact unique clip, samples the existing longest-authored-channel metric grid, and records only finite bilateral model-space stance support. Each retained two-or-more-sample run emits one normalized support window and one earliest-minimum marker. The source primary identity and complete dependency closure are bound into the fragment; an incomplete role, grid, closure, or other prerequisite is a typed exit-1 refusal and leaves any existing output unchanged. --format json writes the same canonical bytes to stdout; text is a presentation summary. The collection form selects one exact manifest logical id and reloads its declared source/config rather than consuming collection-output evidence. Neither form infers physical contact, footsteps, gameplay, IK, or engine behavior.

An output-v19 measure report deliberately has no engine provenance or loader-owned source format. diff also ignores the provenance on lint reports. When its operands are JSON reports, diff validates the complete version-matched records and compares their decoded version-matched measurements (historical v15/v16 or current v18); a selected engine profile does not change that report meaning. When its operands are source assets, the profile is still resolved against each loader-owned source format before measurement.

Runtime-facing attachment/socket/IK nodes have one shared engine-neutral policy:

[runtime_nodes]
selectors = ["weapon_socket", "ik_*_target"]

[checks.rest-world-scale]
expected_uniform_scale = 1.0
uniform_scale_tolerance = 0.0001

The older [checks.rest-world-scale].node_selectors field remains a compatibility alias. Declaring both spellings is a configuration error. An absent selector field or explicit empty list means no runtime-node policy.

Machine Output

measure, lint, collection lint, collection evaluate-directional-speed, diff, generate addressability, and generate import-advice support --format json. The native JSON contract is the source of truth and is versioned with schema_version. See output.md and the current urn:animsmith:schema:output:19. Output-v17 remains available as historical output-v17.schema.json with immutable identity urn:animsmith:schema:output:17. Output-v16 remains available as historical output-v16.schema.json with immutable identity urn:animsmith:schema:output:16. Nested measurement evidence has its own urn:animsmith:schema:measurements:18 measurements-v18.schema.json contract. urn:animsmith:schema:measurements:15, urn:animsmith:schema:output:9, urn:animsmith:schema:output:11, urn:animsmith:schema:output:12, urn:animsmith:schema:output:13, urn:animsmith:schema:output:14, and urn:animsmith:schema:output:15 remain historical immutable contracts alongside output-v16. diff retains strict version-matched readers for output-v11/v12 with measurements-v15 and output-v13 through output-v17 with measurements-v16; the latter retains identity urn:animsmith:schema:measurements:16; output-v9 and its measurements-v9 pairing remain immutable historical contracts. Output-v10 and earlier reports require regeneration from the original asset.

collection lint COLLECTION.toml --format json emits the separate current urn:animsmith:schema:collection-output:11 contract. Historical urn:animsmith:schema:collection-output:9, urn:animsmith:schema:collection-output:8, urn:animsmith:schema:collection-output:7, urn:animsmith:schema:collection-output:6, urn:animsmith:schema:collection-output:5, and urn:animsmith:schema:collection-output:4 remain immutable; the manifest directory is the control root; safe missing/unreadable sources, rejected readable bytes, digest/take mismatches, and incomplete runtime-set members remain typed rows and exit 1 while later safe sources continue. Invalid manifests, unsafe or nonregular paths, and missing/malformed selected configs exit 2 without an envelope. Source, clip, and set rows are canonical; member order remains the manifest order. Each source separately retains its primary input identity and a complete, partial, or unavailable dependency-closure state. Only a complete closure identity can establish that source, one of its clips, or a runtime-set member; partial/unavailable reasons remain typed and make the result incomplete with exit 1. Each established clip binds an exact source take index/name to a normalized clip index and carries duplicate-safe indexed measurements. The nested whole-document lint envelope uses current output v19 and measurements v18; output v18 and measurements v17 remain a matched historical reader pair with immutable identities urn:animsmith:schema:output:18 and urn:animsmith:schema:measurements:17. The preceding collection envelope is also retained as urn:animsmith:schema:collection-output:10. The strict reader preserves version binding for collection-output-v9 with output-v17, collection-output-v8 with output-v16, collection-output-v7 with output-v15, collection-output-v6 with output-v14 and collection-output-v5 with output-v13; older historical contracts remain immutable and are not retargeted. Regenerate current collection evidence before passing it to collection evaluators. Runtime sets keep decision: not_evaluated; they make no blend, controller, engine, artistic, or gameplay claim. Every declared member carries raw root_travel: its existing duration_s, root-trajectory translation availability, signed horizontal X/Z displacement, sampled horizontal travel, and speed availability/value. Binding-unavailable rows remain explicit with unavailable root-travel facts. evidence.root_travel reports the count of fully measured declared members and a complete/incomplete lifecycle; it never reduces the declared set to measurable rows. Gait-group member rows additionally carry raw gait-phase availability, and only a fully established, phase-measured set emits evidence.gait_phase.phase_spread with basis max_circular_deviation_from_mean; this preserves existing gait lint threshold semantics. See collection-output-v7.schema.json.

collection evaluate-directional-speed --policy POLICY.toml --evidence COLLECTION-OUTPUT.json --format json strictly reads a bounded collection-directional-speed-policy:1 declaration and a bounded collection-output V7, historical V6, or historical V5 document, then writes the separate immutable urn:animsmith:schema:collection-directional-speed-evaluation:1 result. The result binds the exact raw TOML and JSON byte identities and preserves every declared member in manifest order. Invalid, stale, wrong-kind, unreadable, malformed, or over-budget inputs exit 2 with no stdout. Incomplete root travel, zero endpoint displacement, zero ratio reference, numeric-range outcomes, and declared-policy findings write the result and exit 1; only a complete passing policy exits 0. It has no text/Markdown presentation, --output, subset, or inference mode. See collection-directional-speed-evaluation-v1.schema.json.

generate addressability has a separate immutable contract, urn:animsmith:schema:gltf-animation-addressability:1; see output.md and gltf-animation-addressability-v1.schema.json. The richer exact-Bevy path additionally has immutable contract urn:animsmith:schema:gltf-addressability:2; see gltf-addressability-v2.schema.json. Neither is output-v11 or a diff measurement operand.

generate contact-fragment is also outside output-v11: it writes the strict urn:animsmith:schema:contact-fragment:1 sidecar. Its exit-1 JSON refusal uses producer-refusal:1; exit 2 is reserved for CLI, config, path, or publication control failures.

generate import-advice requires an exact [engine] selection and all required settings, then binds that resolved profile to one input and the same-load raw facts, dependency closure, explicit clip intent, and normalized measurements:

animsmith --config unity.animsmith.toml generate import-advice export.fbx
animsmith --config unity.animsmith.toml generate import-advice export.fbx \
  --format markdown

The default JSON is the separate immutable contract urn:animsmith:schema:engine-import-advice:1; see output.md and engine-import-advice-v1.schema.json. The strict reader caps the serialized document at 256 MiB before decoding. Unity 6000.3 Generic/Humanoid projects only the resolved importer settings modeled by profile revision 1. Unreal 5.8 and Godot 4.7 revision 1 return a typed profile_settings_unmodeled refusal (exit 1). Available advice exits 0; config, unsupported-profile/input-format, I/O, and serialization errors exit 2. The command never invents authored frame numbers, sampling rates, unit conversion, or root-motion behavior. Text and Markdown are presentation-only views of the same validated value.

The exact revision-2 tuples are also supported by this command. Godot uses godot / 2 / 4.7 / resource-importer-scene with glTF JSON or GLB and projects only animation/fps (1..120, profile default 30) and animation/trimming (profile default false). Unreal uses unreal / 2 / 5.8 / fbx-importer with FBX and requires an explicit sample_rate = "default_30", "source_determined", or "custom_hz(n)" for 1..48,000 Hz. The V2 JSON identity is urn:animsmith:schema:engine-import-advice:2; its basis is bound to the document-scope import-setting projection and its optional projection is typed. An available document carries the projection; a refused document carries a typed reason and no projection. The schema is engine-import-advice-v2.schema.json. Neither state means the engine imported or played the asset: frame ranges, units, skeleton/retargeting, compression, root motion, runtime behavior, and project-file writes remain outside this output.

Measurements v16 retains the v15 clip evidence and adds source-order primitive geometry plus bounded leading-magic evidence for unsupported nonempty images. Measurements v15 added canonical per-bone local TRS channel coverage and sampled Root/Hips trajectory evidence. Root is preferred whenever that role resolves; Hips is only a typed fallback when Root is unresolved. The trajectory groups signed endpoint displacement, sampled horizontal travel and vertical extrema, plus net/unwrapped/travel yaw with independent translation and yaw availability. These are engine-neutral normalized-model-space regression facts from the shared uniform metric grid, not continuous-curve or engine root-motion extraction proof; see output.md for the full coordinate, sampling, and availability contract.

rest-world-scale is quiet until its config resolves a nonempty shared or legacy runtime-node selector policy. Each exact name or * glob must resolve to one source node; findings include the stable source-node path and ancestry so attachment/import policies can be traced back to the source projection. For glTF that projection is authored node state; for FBX it is ufbx-normalized metre/Y-up, adjusted/inheritance- compensated state rather than the raw FBX transform stack. See the selected-node scale workflow.

convert --format json emits conversion evidence v2, with immutable identity urn:animsmith:schema:conversion-evidence:2; see output.md and conversion-evidence-v2.schema.json. It records the requested options, counts from the written artifact, exact static-mesh transforms when requested, and recipe provenance when a material texture recipe is used. text is the default human-readable write summary. An asset refusal under --format json uses the independent immutable producer-refusal-v1.schema.json identity instead; conversion evidence v2 remains success-only and unchanged.

assemble writes evidence v7 to its required --evidence path, with immutable identity urn:animsmith:schema:character-assembly-evidence:7; see character-assembly-evidence-v7.schema.json. assemble --format json prints the same record to stdout — the identical bytes the evidence file receives, serialized once — in place of the default text publication summary. An asset refusal publishes neither member and exits 1; JSON emits producer-refusal:1 on stdout, while text emits its stable kind on stderr. Operator errors remain exit 2, stderr-only. A failed stdout delivery does not change an already-established success or refusal code.

scale writes scale evidence v4 for glTF/GLB to its required --evidence path, with immutable identity urn:animsmith:schema:scale-evidence:4; see output.md and scale-evidence-v4.schema.json. The same record is what scale --format json prints to stdout, for a refusal as well as for a published pair. For an artifact-proof-failed refusal from the exact-preservation walk, rejection.artifact_proof_differences names up to 16 raw JSON locations the walk found different; omitted counts locations beyond that fixed cap, and the full count is items.length + omitted. The field is null for other artifact-proof claims and for capability refusals, whose violations array retains its existing meaning.

With the default FBX feature, scale rest-bind INPUT.fbx -o OUTPUT.glb is a separate narrow producer contract. It accepts only the complete normalized ufbx inventory, emits the immutable urn:animsmith:schema:scale-evidence:5 record described by scale-evidence-v5.schema.json, and proves the exact re-encoded GLB after reload. It never writes FBX and makes no raw-FBX preservation claim. scale whole-document remains glTF/GLB-only.

Machine-readable lint rejects --allow so it cannot erase evidence. The flag remains a presentation and exit-policy convenience for text and Markdown.

Native JSON is deliberately shaped so serializers can be added later without redesigning the checks: SARIF for code scanning, GitLab Code Quality/CodeClimate for MR widgets, JUnit XML for CI dashboards, and CSV for ad-hoc analysis.

Each JSON measure and lint file row records an input SHA-256 and byte count for the exact primary file bytes parsed. Keep that row with retained review, promotion, or publication evidence; multi-file runs calculate one identity per argument in order. For .gltf, the identity deliberately does not cover external buffer or image files. Profiled lint embeds the bounded same-load dependency closure in prediction_provenance; other workflows must retain dependency provenance separately when that scope matters.

Human-readable command output keeps asset-derived names, messages, and paths on visibly ordered lines: terminal controls, Unicode line separators, and bidirectional formatting characters are rendered as visible escapes. Markdown also flattens line separators and neutralizes table/code-span delimiters before the text is pasted into a trusted review comment. JSON retains the original strings as machine data.

measure --format json also inventories glTF/GLB material definitions, texture-to-image links, and image metadata. Its exact core source-slot domain is base_color, normal, metallic_roughness, occlusion, then emissive. This lets a pipeline distinguish an emissive-only material from an untextured one and identify shared source resources without parsing the asset again. The nested contract reports material_resource_coverage: "complete" when that documented five-slot glTF/GLB domain was inspected and "unavailable" when a loader has no equivalent source-resource view. Complete does not cover extension-defined texture slots or promise writer/conversion preservation. This is descriptive evidence only: it does not accept, repair, resize, transcode, or otherwise fix an image. See machine-readable output for the full boundary.

CI Comments (lint --format markdown)

lint --format markdown renders findings as GitHub/GitLab-flavored Markdown for pasting into a CI comment or asset-review thread. It mirrors the text output's information — severity, check id, location, measured and expected values, per-clip grouping, and typed coverage gaps — as tables inside a per-file collapsible section, with a clean summary for a passing asset:

animsmith lint clip.glb --format markdown >> "$GITHUB_STEP_SUMMARY"

A file with findings is rendered as a <details> section (expanded for short lists, collapsed once a file carries more than ten findings so one noisy asset does not bury the rest of the comment). A file with neither findings nor gaps collapses to a one-line ✅ Clean summary. A footer tallies errors, warnings, notes, and gaps across every input. The exit code is unchanged from text and JSON — see Exit Codes. Repeated gaps with the same check id and code share one presentation row with a count and bounded subject list; JSON retains every original scope.

Markdown is presentation-only and carries no stability guarantees — gate automation on --format json (see output.md), and treat the Markdown as display text that may be re-laid-out between releases.