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

Why animsmith

What animsmith is, why it exists, and what it is worth to your team.

This page is for someone deciding whether to adopt animsmith — a lead evaluating tools, a producer weighing pipeline changes, an engineer asked "should we use this?". If you already use animsmith and want reference material, start from all pages instead.

What animsmith is

animsmith validates, measures, reports, and prepares skeletal animation clips for game-engine import and runtime use.

Concretely: point it at a glTF/GLB or FBX export and it answers — before engine import, in seconds, with frame- and bone-level detail — questions that otherwise wait for a playtest: Does the loop pop? Do the feet slide? Does the measured root-motion speed match what the character controller expects? Did this re-export silently change the motion? It works from the command line, from CI via stable exit codes and versioned JSON, or from Rust as a library — and it repairs the mechanical problems that are provably safe to fix.

Why it exists

Every game team answers "is this clip game-ready?" by hand today: a tech artist scrubbing a timeline, a reviewer squinting at a playblast, a bug report from playtest. Nothing open-source does game-semantics clip validation. Format validators stop at spec conformance — a run cycle whose feet skate across the ground is perfectly valid glTF; the validator has no concept of a foot, a loop, or root motion. Engine importers do see content problems, but they see them late, inside editor workflows, after import-time conversion has already reshaped the data — and every studio re-derives its custom import checks from scratch. Academic motion-metric code lives in ML-evaluation repos, not artist tools.

animsmith is the missing layer in between: a content quality gate and repair assistant for animation clips — the linter and test suite counterpart to engine import inspectors, packaged as a standalone Rust library and CLI: glTF/GLB native, FBX ingested for DCC exports, engine-agnostic, with machine-readable output for CI. It gives animation teams the same repeatable, reviewable, CI-friendly confidence that code teams expect from tests and linters.

What "game-ready" means

"Game-ready" is staged evidence, not a stamp — a clip is ready for a consumer, and the last stages of readiness belong to that consumer. The readiness ladder is the canonical definition; it stages the evidence from file-ready data to shipped acceptance. What animsmith contributes splits three ways:

  • Validated generically, on every file: the data is mechanically sound — finite values, unit rotations, monotonic key times, sane durations, no accidental scale animation or export bloat — and the losslessly repairable defects are repaired.
  • Validated against your declarations: the semantic contracts only you can state — loops, in-place policy, speed pins, required bones, blend sets — declared in a project config, resolved through a rig profile, with missing prerequisites recorded as typed, nonblocking coverage gaps rather than guessed.
  • Consumer-owned: importer and blend-graph behavior, controller feel, artistic quality, and shipping sign-off. animsmith's reports and measurements are evidence for that review, not a substitute for it — no standalone tool can certify behavior inside a runtime it has never seen.

What game-ready means covers the measurable ground symptom by symptom: the runtime failure you see when a declared contract is violated, the mechanics behind it, and the check, repair, and config that address it.

What it is worth, by role

Artists and animators get actionable frame- and bone-level reports instead of "the import looked wrong": which clip, which bone, which frame, and a fix hint that maps to a DCC action — trim frames, freeze scale, rename bones, fix the rest pose. animsmith lint export.fbx runs seconds after export, while the DCC session is still open.

Technical animators codify engine and rig assumptions once — loop flags, speed pins, blend rings, rig profiles — in a project config, then batch-validate whole libraries against it instead of re-checking every delivery by hand.

Gameplay engineers stop debugging capsule, root-motion, and foot-slide problems after import. Root travel, stride, and gait phase become measured numbers checked against declared tolerances, and CI gates catch a drifted re-export before it reaches a playtest.

Producers and pipeline owners turn outsourced-asset acceptance into measurable requirements: a machine-readable acceptance contract that both sides can run, fewer review round-trips, and asset state that is visible in a report rather than in someone's head.

Tool engineers build on a Rust library and a versioned JSON envelope with published schemas, instead of scraping engine import logs — the same checks the CLI runs, inside your own pipeline.

What animsmith is not

animsmith is not a converter, an animation editor, or a DCC replacement. It converts FBX exports to glTF as pipeline plumbing and applies byte-surgical repairs whose correctness its own checks can verify, but artistic transformation — retargeting, motion editing, re-authoring — is DCC work and stays out of scope. It also does not replace glTF-Validator or engine importers: it sits between them, checking the content semantics the format validator cannot see, earlier and more repeatably than the engine can.

Where to go next

Convinced, or curious? The what game-ready means covers each failure mode in depth — mechanics, check, repair, config — and everything else, from install to runnable workflows, is routed from the all pages.

What game-ready means

What animsmith checks for, and why exported clips fail at runtime.

A skeletal animation clip can pass every format validator and still break the game it ships in: the loop pops, the character glides, feet skate through blends, a limb stays T-posed. Those are not file-format errors — the file is spec-conformant — they are content problems that only surface after the slowest step of the pipeline: engine import, a bake, a playtest.

This page defines what "game-ready" means here. The readiness ladder below stages the evidence from file-ready data to shipped acceptance and says who owns each level. The checks, repairs and config surfaces themselves live in the symptom index, which routes every runtime symptom — and the narrower presentations of one — to the page that walks it, and carries every registered check id in one table.

Each symptom has its own page under symptoms: what you see in the engine, what AnimSmith measured, the finding, the repair, and the precise contract one click down. If you want runnable commands, use the examples cookbook; if you want the current per-check reference for IDs, default findings, prerequisites, config keys, coverage gaps, and remediation boundaries, see the built-in check reference; if you want the reasoning behind the tool itself — why it exists and what it is worth to your team — see why animsmith.

A valid file is not a usable clip

Format validators — Khronos glTF-Validator being the canonical one — check spec conformance: accessor validity, buffer bounds, quaternion norms at the container level. They have no concept of a loop, a gait, or root motion. A clip whose walk cycle was cut a quarter-stride short is perfectly valid glTF; it will also visibly pop every second of gameplay.

The characteristics below fall into two groups, and animsmith treats them differently:

  • Mechanical characteristics hold for every clip, with no knowledge of your project: finite values, monotonic key times, unit quaternions, consistent durations.
  • Semantic characteristics are contracts only you can declare: this clip loops, this one is authored in place, these four form a blend ring — declared in a project config and resolved through a rig profile.

These two groups are the first two levels of the readiness ladder, which states what animsmith does about each.

Two loops benefit. The artist inner loop — animsmith lint export.fbx seconds after a DCC export catches "the loop pops" or "wrong rig" while the DCC session is still open, instead of after import and bake. And the CI gate — the same checks with machine-readable output and stable exit codes hold every committed asset to the contract, so a re-export can't silently drift.

The readiness ladder

"Game-ready" is not one property a tool can certify, because most of it is relative to a consumer: your engine, your controllers, your bar for quality. It is a ladder of evidence, and each level has a different owner. animsmith's job is to make the early levels checked and repeatable, make the declared middle measurable, and say plainly what it did not evaluate — not to stamp the whole ladder.

  1. File-ready — the data is parseable, finite, and mechanically valid: no NaN/Inf, monotonic key times, unit quaternions, sane durations, clean track hygiene. This is animsmith's primary generic coverage: the mechanical checks (nan, time-monotonic, quat-norm, quat-flip, duration-sanity, scale-keys, non-uniform-scale, constant-track) run on every file with no configuration. The narrower constant-nonunit-scale policy signal is registered but opt-in. fix repairs the two losslessly repairable defect classes (quat-norm, quat-flip).

  2. Clip-ready — the clip honors what you declared about it: loop closure, duration and frame grid, in-place vs root-motion policy, required bone motion, structural rig presence, bind-pose consistency. Strong, config-backed coverage where a check exists: fps, loop-seam, in-place, root-motion-speed, foot-slide, missing-bones, required-bones, frozen-bone, and bind-pose judge exactly the expectations you declare — and the checks that need rig roles report a typed coverage gap instead of guessing when a role cannot be resolved. One member is heuristic: foot-slide ships as a warning (see feet slide within one clip).

  3. Set-ready — clips that blend or sync together are compatible as a set. Generic measurement and checking where implemented: gait-group holds a declared directional blend ring to a shared stride phase, sync-group checks same-time timing surfaces, and time-complement explains pairs whose stride phase aligns materially better under reflected time. measure supplies the per-clip numbers, and animsmith diff catches drift between revisions. Set compatibility beyond the implemented checks is yours to review.

  4. Rig and use prerequisites — which bones play which roles on the target rig, which bones must exist, which bones a clip must animate, and what each clip is for. A shared boundary: you supply the meaning (a rig profile or [rig.roles], [rig] required_bones, animates_bones, per-clip expectations), and animsmith resolves roles against the skeleton, checks the declarations, and reports the resolved roles it used. Nothing at this level can be inferred from the file alone.

  5. Runtime integration — importer behavior, blend-graph topology, animation target IDs, masks, sync and reset behavior, and the poses your engine actually evaluates. Consumer-owned: animsmith's exact Bevy 0.19.0 profiles can predict the canonical Animation{i} source-animation selector and, in the current revision 3 slice, negative animation/channel gate outcomes. It does not run the engine, prove runtime asset existence, or validate graph wiring, target survival, or positive playback. A dropped source row is prediction evidence, not a content finding; when both gates allow loading, runtime survival is required-unavailable. Its measurements come from its own documented sampling model — a model of engine samplers, not a reproduction of yours.

  6. Gameplay, artistic, and production acceptance — controller feel and timing, readability, visual quality, provenance, reproducibility, shipping sign-off. Consumer-owned: reports and measurements inform the review; people make the call.

A clean run is evidence, and evidence has scope: it covers the checks that ran, on the file that ran, against the contract you declared. Only an actual animsmith run on the actual file establishes that evidence — nothing transfers from vendor previews, other files in the pack, or another export's report. And where generic validation touches a later level, it supplies prerequisites or evidence for that level, never blanket certification of it: a mechanically pristine, contract-clean clip can still be rejected by your importer, your blend graph, or your art director.

Reading a lint run

One animsmith lint run answers five independent questions. Keep them separate when you automate on the output:

  • Was the check active? The full catalog is selected by default; --select narrows the selected set, and [checks.<id>] severity = "off" disables a check. A built-in opt-in check stays disabled until its severity is set to "note", "warn", or "error". Final JSON still records inactive checks without executing them.
  • Did it apply here? Contract-aware checks judge only declared expectations. With no loop = true clip in the config, loop-seam has nothing to judge and is recorded as not_applicable.
  • Was the work evaluated? When declared work exists but a prerequisite or measurement is missing, the check reports a typed coverage gap. A check can also complete part of its work: gait-group still validates declared ring members when unresolved roles keep it from measuring phase, then reports member existence as completed and phase coherence as a gap. Engine prediction work uses a separate required_prediction_unavailable facet; unlike an ordinary coverage gap, it makes lint exit 1 and cannot be suppressed with severity or --allow.
  • What did the evaluated work find? Content findings at note, warning, or error severity, carrying clip, bone, time, and measured-vs-expected context.
  • What blocks? Gate policy is yours, not animsmith's verdict: exit 1 on error findings, --deny-warnings to promote warnings, per-check severity overrides, and presentation-only --allow in text or Markdown. Coverage gaps never fail a run, while required-unavailable engine prediction work does. Exit 0 means no failing findings and no required prediction work remains unavailable; it still does not imply ordinary gap completeness. A gate that requires full coverage must inspect gaps too.

There is deliberately no single "pass" state: a run can complete with warnings, and it can evaluate some declared work while skipping the rest. See machine-readable output for the current v19 representation. It models selection, configuration, applicability, and evaluation independently, keeps content findings separate from typed gaps, and records completed work scopes. This is evidence about animsmith's checks, not runtime certification; stricter completeness policy belongs to the consuming pipeline.

Each clip symptom the levels above describe has a page of its own under symptoms, whose one table routes it to the checks that measure it, the repair, the config surface, and who owns the fix; the three runtime problems that are not about a clip are answered on that index itself.

Why animsmith exists

The positioning case — what animsmith is, why nothing else fills this role, and what it is worth to each role on a team — lives in why animsmith.

Everything else — the symptom pages themselves, runnable workflows, pipeline scenarios, the CLI reference, embedding, and the dated engine survey behind this page's contract — is routed from all pages.

Install

AnimSmith is one binary. Pick the route that fits your machine, then confirm it runs.

Prebuilt binary

Download the archive for your platform from the latest GitHub release. The archive names for each platform are listed in the CLI reference. Each archive ships with a matching .sha256 file, so you can verify the download before unpacking it:

$ sha256sum -c animsmith-vX.Y.Z-x86_64-unknown-linux-gnu.tar.gz.sha256
$ tar -xzf animsmith-vX.Y.Z-x86_64-unknown-linux-gnu.tar.gz

Put the animsmith binary somewhere on your PATH.

Cargo

With a Rust toolchain installed:

$ cargo install animsmith

The default build includes FBX input and HTML reports. For a pure-Rust, glTF-only binary with no C build step:

$ cargo install animsmith --no-default-features

That build omits report, convert, and assemble; everything else works.

Check it

$ animsmith --version   # exits 0

Rust pipelines

If you would rather call the checks from your own Rust code than shell out to the binary, start with the embedding guide; the crates are on crates.io and documented on docs.rs.

The project README is the same install summary in the form crates.io and GitHub show, plus a condensed check and configuration reference.

Next: first lint in 60 seconds.

First lint in 60 seconds

Two synthetic clips from this repository are enough to see what a finding looks like, repair one, and watch a declared contract catch a popped loop. The commands run from a checkout of the repository. With an installed binary and no checkout, download the files they name from examples/assets/ and examples/walk.animsmith.toml and adjust the paths.

1. Lint a clip that flickers

Mechanical checks need no configuration. This clip carries one non-unit rotation key and one hemisphere flip, the two representation defects that make a pose flicker or spin the long way round:

$ animsmith lint examples/assets/clip-dirty.glb
examples/assets/clip-dirty.glb:
  error[quat-norm] clip 'swing' bone 'spine' @0.500s: non-unit rotation key (worst at key 2) (measured 1.0500, expected 1.0000)
  warning[quat-flip] clip 'swing' bone 'spine' @0.750s: 2 hemisphere flip(s) between adjacent rotation keys (first between keys 2 and 3) — engines without neighborhood correction will spin the long way (measured 2.0000)
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: swing): only 1 usable first-frame rotation track(s); at least three are required
1 error(s), 1 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 1

Every finding names the check, the clip, the bone and the time, then the measured value against the expected one. The coverage line is not a finding: a check that lacked the evidence it needs (here, at least three rotation tracks) says so instead of guessing, and never fails the run.

2. Repair what is mechanically safe

Both defects are lossless to repair, so fix can do it. --dry-run reports the pending repairs and exits 1 without writing anything; -o writes the repaired copy.

$ animsmith fix examples/assets/clip-dirty.glb --dry-run
  would fix[quat-norm] clip 'swing' bone 'spine': 1 key(s) unit-normalized
1 key(s) would be fixed across 1 track(s) -> no output written
  would fix[quat-flip] clip 'swing' bone 'spine': 1 key(s) hemisphere-normalized
1 key(s) would be fixed across 1 track(s) -> no output written   # exits 1

$ animsmith fix examples/assets/clip-dirty.glb -o fixed.glb
  fixed[quat-norm] clip 'swing' bone 'spine': 1 key(s) unit-normalized
1 key(s) fixed across 1 track(s) -> fixed.glb
  fixed[quat-flip] clip 'swing' bone 'spine': 1 key(s) hemisphere-normalized
1 key(s) fixed across 1 track(s) -> fixed.glb   # exits 0

$ animsmith lint fixed.glb
fixed.glb:
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: swing): only 1 usable first-frame rotation track(s); at least three are required
0 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 0

The repair changes only the bytes of the keys it names; meshes, skins and every other accessor pass through byte-identical, and animsmith diff reports no measurement moved.

3. Lint against a contract

Whether a clip loops is your knowledge, not the file's. Without a declaration, this walk cycle lints clean:

$ animsmith lint examples/assets/walk-dirty.glb
examples/assets/walk-dirty.glb:
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: walk): only 0 usable first-frame rotation track(s); at least three are required
0 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 0

Declare it as a loop through a project config and the seam checks arm:

$ animsmith --config examples/walk.animsmith.toml lint examples/assets/walk-dirty.glb
examples/assets/walk-dirty.glb:
  error[loop-closure] clip 'walk' bone 'foot_r' @1.000s: loop does not close in position: bone 'foot_r' is 0.1581 m from its first-frame model-space position (cap 0.0100 m) (measured 0.1581, expected 0.0100)
  error[loop-seam] clip 'walk' @1.000s: loop seam pops: wrap discontinuity is 6.82× the neighbouring in-clip step (cap 1.60) — the clip does not close its cycle (measured 6.8152, expected 1.6000)
  error[loop-seam-vel] clip 'walk' bone 'foot_r' @1.000s: loop velocity changes at the seam: bone 'foot_r' differs by 0.7972 m/s between the incoming and outgoing model-space velocities (cap 0.1000 m/s) (measured 0.7972, expected 0.1000)
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: walk): only 0 usable first-frame rotation track(s); at least three are required
3 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 1

The clip was cut a quarter-cycle short, so the right foot is 16 cm from where the loop needs it to be. The config file is short and commented; most of the loop caps it sets are the defaults, and it says where it departs from them — a stricter 0.5 degree per-clip rotation cap (default 1.0) and a 1.6 loop-seam ratio (default 1.5). Which parts of such a contract you can declare on your own, and which need the game developer, is who writes what.

4. Read the exit code

0 means no failing findings (warnings, notes and coverage gaps may remain), 1 means failing findings, 2 means an operator error such as a missing file. --deny-warnings promotes warnings to failures for a stricter gate.

Next: your first report shows the same findings as skeleton playback and charts. When something specific looks wrong in the engine, start from the symptoms.

Your first report

A finding tells you which clip, bone and frame are wrong. The HTML report shows you: it plays back the exact pose-grid frames the checks judged, with foot and root trails, metric charts, and a findings list that scrubs the viewer to the judged frame when you click it. One file, no server, no dependencies; attach it to a pull request or send it to whoever owns the fix.

The commands below run from a checkout of the repository; the clips are the same synthetic samples used in first lint.

Render the report

$ animsmith --config examples/walk.animsmith.toml report examples/assets/walk-dirty.glb -o report.html
wrote report.html (1 clip(s), 3 finding(s), 0.1 MB)   # exits 0

Open report.html in a browser. This is the same report, embedded here and scrubbed to the first finding:

Open it full size. Drag to orbit, use the wheel to zoom, and click a finding to jump to its frame. When the file holds more than one clip, the with control plays a second one beside the first at the same normalized phase, with both source times labelled — a presentation mapping between two timelines, not a retime. With a pair selected, enable illustrative blend for an optional third pane and a continuous weight slider. This engine-agnostic presentation interpolates sampled local transforms and runs forward kinematics; it adds no measured finding or runtime evidence. See the blend contract. The source charts share a playhead with the 3D view, and each chart caption says what to look for in it — derived from what your configuration declares for that clip and what the checks judged, not from an assumption about what the clip is. A clip declared loop = true is told its curves should end where they began; one that pins speed_mps with animation-owned travel is told the root should keep travelling at that speed, with no claim about the shape it travels; a clip that declares neither is told so, and named the checks that judged it. A root path is also marked where the track starts (a hollow circle) and where it ends (a filled square), and the caption states how far apart the two are. A configuration that declares a [gait_groups.<name>] blend ring adds one further figure per group, drawing every member's left-minus-right foot height on one shared stride-phase axis with the declared tolerance as a band; that figure stays on screen whichever member you select, because comparing them is what it is for.

Compare before and after

When a clip has been repaired or re-exported, render both versions into one comparison report. The pair below is a synthetic gait with a seam and a sliding foot on the left, and the corrected clip on the right:

$ animsmith --config examples/report-comparison.animsmith.toml report \
    examples/assets/report-comparison-before.glb \
    --compare-after examples/assets/report-comparison-after.glb \
    --before-clip acceptance-matrix --after-clip acceptance-matrix \
    -o comparison.html
wrote comparison.html (1 clip(s), 5 finding(s), 0.1 MB)   # exits 0

Open the comparison. Press play beside the shared phase to run both sides together, or scrub it by hand. Tick Overlay after on before beside it to draw the two skeletons in one pane — the before solid, the after dashed over it — at those same frames. The judged poses come first, then the shared root trajectory — fitted to its own panel, with a scale bar in the corner and the magnification against the role-trajectory panels in its caption, so a two-centimetre sway is legible and still reads as two centimetres — and then each side's trails and gait. The gait panel shades the sampled stance intervals, and every panel's caption says what to look for in it.

Share evidence without sharing the asset

The report embeds the sampled pose grid, which is the motion itself. When the clip is licensed or confidential, render an evidence-only report: findings, coverage gaps, engine predictions and charts stay, the poses are omitted, and the file is safe to attach to a vendor ticket.

$ animsmith --config examples/walk.animsmith.toml report examples/assets/walk-dirty.glb -o evidence.html --evidence-only
wrote evidence.html (1 clip(s), 3 finding(s), 0.1 MB)   # exits 0

Where next

Mixamo to game-ready: an end-to-end tutorial

Take one free Mixamo download through the whole pipeline: download, convert to glTF, inspect the rig, lint, fix what is mechanically fixable, and grow a project contract config from the results.

This is the bring-your-own-asset counterpart to the examples cookbook: the cookbook runs against small assets committed to this repo, while this tutorial runs against a real marketplace rig you download yourself. Each step links to the page that owns its why rather than re-explaining — what game-ready means for everything the checks judge, and the pipeline scenario guide for acquiring and handling the raw asset.

The complete key, default, and precedence reference for the contract used below is the configuration reference.

About the transcripts. The command output in this tutorial is real, captured from a small generated rig that carries the same mixamorig:* bone names, the same nine profile roles, and the same mixamo.com take name as a real download (this repo does not ship Mixamo's own files — see the asset policy). Your download will show many more bones — fingers, twist joints — and possibly more findings; the roles, the commands, and the behavior are the same. Excerpts marked real download were captured once (July 2026) from Mixamo's default Walking; unlike the stand-in transcripts they are not regenerated by this repo's tests, so treat their exact numbers as a snapshot.

Build requirements. convert (step 2) ships in the default install — it sits behind the fbx feature, which is enabled unless you opted out. If you installed with --no-default-features, use the default build for this one step (see feature flags). Everything from step 3 on works in any build.

1. Download a clip from Mixamo

Mixamo is free with an Adobe ID, and Adobe documents its animations as royalty-free for personal and commercial projects — see Adobe's Mixamo FAQ for the current terms. That covers using the assets in your project; it does not clearly permit redistributing the downloaded files, so keep them out of public repos and do not commit them here.

  1. Sign in and pick any character.
  2. Search the animations for Walking and select a walk cycle.
  3. Check the In Place option. Mixamo's default walks travel through the world; In Place keeps the hips over the origin, which is the shape most capsule-driven character controllers expect. (If your game uses root motion, download without In Place — step 6 shows how the contract differs.)
  4. Download as FBX Binary with skin. The skin is optional for linting, but keeping it lets report render the mesh later.

We recommend keeping the downloaded FBX as your immutable raw source and treating every animsmith output as a derived artifact — the pipeline scenario guide explains that raw-vs-generated split.

2. Convert to glTF

$ animsmith convert Walking.fbx -o walking.glb    # real download
wrote walking.glb (70 node(s), 1 clip(s), 2 mesh(es) / 28374 position(s), 2 material(s)); dropped 1 clip(s) with no writable tracks

(Counts vary by character. This download contains a second take named Take 001 with zero tracks and keys. Conversion omits that empty take, writes the animated mixamo.com clip, and reports the omission. The licensed download is not committed here.)

animsmith lints FBX directly, so conversion is not required to run checks — but glTF/GLB is the native format for the rest of the pipeline (fix, transform, and diff operate on glTF), and a convert-once step gives later commands a stable baseline to compare against. A conversion that succeeds proves the container is well-formed, not that the motion is usable — a valid file is not a usable clip — which is what steps 4–6 judge.

3. Inspect the rig

$ animsmith inspect walking.glb
walking.glb
rig profile: mixamo (9 roles)
  hips         -> mixamorig:Hips (exact)
  spine        -> mixamorig:Spine (exact)
  head         -> mixamorig:Head (exact)
  left_foot    -> mixamorig:LeftFoot (exact)
  right_foot   -> mixamorig:RightFoot (exact)
  left_toe     -> mixamorig:LeftToeBase (exact)
  right_toe    -> mixamorig:RightToeBase (exact)
  left_hand    -> mixamorig:LeftHand (exact)
  right_hand   -> mixamorig:RightHand (exact)
skeleton: 9 bones
  mixamorig:Hips
    mixamorig:LeftFoot
      mixamorig:LeftToeBase
    mixamorig:RightFoot
      mixamorig:RightToeBase
    mixamorig:Spine
      mixamorig:Head
      mixamorig:LeftHand
      mixamorig:RightHand
materials: 0
mesh instances: 0
clips: 1
  mixamo.com: 1.000s, 2 tracks, 33 keys max

These are the same nine roles a real download resolves; your skeleton list will just be longer. From the real download (abridged):

$ animsmith inspect walking.glb                   # real download
walking.glb
rig profile: mixamo (9 roles)
  ...the same nine-role table as above...
skeleton: 70 bones
  <fbx-root>
    mixamorig:Hips [skinned]
      mixamorig:Spine [skinned]
        ...
materials: 2
  ...the exact quoted source material names...
mesh instances: 2
  node "<first mesh node name>"
    ...mesh, skin, and primitive/material context...
  node "<second mesh node name>"
    ...mesh, skin, and primitive/material context...
clips: 1
  mixamo.com: 1.033s, 156 tracks, 32 keys max

The extra bones — fingers, twist joints, end markers — carry no role, and the two mesh nodes ride along for report playback.

Two things to read off this output:

  • The mixamo profile resolved without any config. Mixamo is a built-in rig profile: it binds the semantic roles from Mixamo's mixamorig:* bone names, which is what arms the role-based checks in step 6. Without a resolved rig, those checks skip rather than guess (see the symptom index).
  • The clip is named mixamo.com. Mixamo names every export's animation take mixamo.com, and animsmith uses the take name as the clip name. Your contract config must address the clip by this name.

The mixamo profile has no Root role. Mixamo rigs have no dedicated root bone — mixamorig:Hips is the top of the hierarchy, and any root motion is baked into it. animsmith therefore judges in-place and root-motion-speed on the Hips track for Mixamo rigs: travel measurements still work, but they describe hip movement, not a separate locomotion root. If your engine workflow retargets hip motion onto a dedicated root bone (a common Unreal step), that happens downstream in your DCC or importer — lint the export you actually ship to the engine.

4. Lint the mechanical checks

$ animsmith lint walking.glb
walking.glb:
  coverage[bind-pose] first_frame_rest_delta 'mixamo.com': insufficient_rotation_evidence: ...
0 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s)

With no config, expectation-driven checks are not applicable. Mechanical checks still evaluate, as does bind-pose; this analytic fixture has no rotation tracks, so bind-pose records a nonblocking coverage gap rather than claiming a clean evaluation. Findings like constant-track notes are normal on a marketplace rig. From the real download (abridged):

$ animsmith lint walking.glb                      # real download
walking.glb:
  warning[scale-keys] clip 'mixamo.com' bone 'mixamorig:Hips': scale animation present ...
  note[constant-track] clip 'mixamo.com' bone 'mixamorig:Spine': translation track has 2 keys but never moves — export bloat
  ...
0 error(s), 1 warning(s), 103 note(s), 1 coverage gap(s)

Of the clip's 156 tracks, 103 never move — pure export bloat — and the hips carry a scale track worth checking before retargeting. Nothing here is broken motion; the serious mechanical defects — flicker-inducing quaternion problems — tend to creep in later, after the asset has been through your DCC, a retargeter, or an exporter plugin.

5. Fix what is mechanically fixable

$ animsmith fix walking.glb --dry-run
0 key(s) would be fixed across 0 track(s) -> no output written
0 key(s) would be fixed across 0 track(s) -> no output written

(One summary line per default repair — quat-norm, then quat-flip.) fix applies only repairs that are provably lossless: quat-norm and quat-flip, the representation defects behind the pose flickers, spins, or explodes. That makes it safe to run unconditionally; on a clean export it is a no-op, as here. When a re-export does pick up defects:

$ animsmith fix walking.glb -o walking-fixed.glb
$ animsmith lint walking-fixed.glb
$ animsmith diff walking.glb walking-fixed.glb

The re-lint proves the findings are gone; the diff proves the repair moved no measured motion. The cookbook's repair section shows the full transcript of that loop on a committed dirty asset.

6. Grow the contract config

The mechanical checks passing does not make the clip game-ready — a walk whose loop pops or whose feet skate is mechanically pristine (see a valid file is not a usable clip). The semantic checks need your expectations declared. Start from what measure reports. The shortened JSON below is illustrative; capture and retain the exact numbers from your own downloaded asset rather than treating these sample values as vendor evidence:

$ animsmith measure --format json walking.glb
{
  "schema_version": 19,
  "schema": "urn:animsmith:schema:output:19",
  "tool": { "name": "animsmith", "version": "0.14.0",
            "source": { "revision": null, "dirty": null } },
  "command": "measure",
  "summary": { "files": 1 },
  "files": [
    {
      "path": "walking.glb",
      "input": {
        "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
        "bytes": 123456
      },
      "rig": { "profile": "mixamo", "resolution_outcome": "resolved",
        "resolved_roles": { "hips": "mixamorig:Hips", "spine": "mixamorig:Spine",
          "left_foot": "mixamorig:LeftFoot", "right_foot": "mixamorig:RightFoot" },
        "resolved_role_policies": { "hips": "exact", "spine": "exact",
          "left_foot": "exact", "right_foot": "exact" } },
      "measurements": {
        "schema_version": 18,
        "schema": "urn:animsmith:schema:measurements:18",
        "clips": { "mixamo.com": {
          "duration_s": 1.0, "frame_count": 33,
          "animated_bones": ["mixamorig:Hips", "mixamorig:LeftFoot", "mixamorig:RightFoot"],
          "bone_channels": [
            { "bone_index": 0, "bone_name": "mixamorig:Hips",
              "availability": "measured",
              "properties": ["translation", "rotation"] },
            { "bone_index": 1, "bone_name": "mixamorig:LeftFoot",
              "availability": "measured",
              "properties": ["translation"] },
            { "bone_index": 2, "bone_name": "mixamorig:RightFoot",
              "availability": "measured",
              "properties": ["translation"] }
          ],
          "bone_rotation_range_deg": { "mixamorig:Hips": 3.2 },
          "loop_continuity": { "bones": [
            { "bone_index": 0, "bone_name": "mixamorig:Hips",
              "position_delta_m": 0.0, "rotation_delta_deg": 0.0,
              "seam_velocity_delta_mps": 0.0,
              "seam_angular_velocity_delta_degps": 0.0 },
            { "bone_index": 1, "bone_name": "mixamorig:LeftFoot",
              "position_delta_m": 3.7e-17, "rotation_delta_deg": 0.0,
              "seam_velocity_delta_mps": 0.0,
              "seam_angular_velocity_delta_degps": 0.0 },
            { "bone_index": 2, "bone_name": "mixamorig:RightFoot",
              "position_delta_m": 3.7e-17, "rotation_delta_deg": 0.0,
              "seam_velocity_delta_mps": 0.0,
              "seam_angular_velocity_delta_degps": 0.0 }
          ] },
          "loop_continuity_availability": "measured",
          "loop_endpoint_mode_availability": "not_applicable",
          "frame_grid_availability": "not_applicable",
          "loop_seam_ratio": 1.2e-15,
          "loop_seam_ratio_availability": "measured",
          "gait": { "phase": 0.75, "phase_availability": "measured", "lr_amplitude_m": 0.2 },
          "gait_availability": "measured",
          "root_trajectory": {
            "bone_index": 0, "bone_name": "mixamorig:Hips",
            "source_role": "hips_fallback",
            "translation": {
              "horizontal_displacement_x_m": 0.0,
              "horizontal_displacement_z_m": 0.0,
              "horizontal_travel_m": 0.0,
              "vertical_displacement_m": 0.0,
              "vertical_min_displacement_m": 0.0,
              "vertical_max_displacement_m": 0.0
            },
            "translation_availability": "measured",
            "yaw": {
              "heading_axis": "positive_z", "net_yaw_deg": 0.0,
              "unwrapped_yaw_deg": 0.0, "yaw_travel_deg": 0.0
            },
            "yaw_availability": "measured"
          },
          "root_trajectory_availability": "measured",
          "speed_mps": 0.0,
          "speed_mps_availability": "measured"
        } },
        "mesh_definitions": [],
        "node_instances": [],
        "scenes": [],
        "skeleton_source_coverage": "unavailable",
        "skeleton_nodes": [],
        "skins": [],
        "material_resource_coverage": "complete",
        "material_definitions": [],
        "textures": [],
        "images": []
      }
    }
  ]
}

(Abridged — all nine roles resolve; head, toes, and hands are elided here.) The numbers become the contract: near-zero per-bone loop deltas and loop_seam_ratio say the cycle closes in pose, velocity, and locomotion phase; speed_mps of zero confirms the In Place download, and the gait numbers seed a blend-ring group once you have more than one direction. Declare what must stay true by saving this as animsmith.toml next to your clip — animsmith auto-loads it from the working directory:

[rig]
profile = "mixamo"

[clips."mixamo.com"]
loop = true
movement_owner_xz = "gameplay"

[checks.loop-seam]
max_ratio = 1.6

[checks.loop-closure]
max_position_delta_m = 0.01
max_rotation_delta_deg = 1.0

[checks.loop-seam-vel]
max_velocity_delta_mps = 0.1

[checks.loop-seam-rot]
max_angular_velocity_delta_degps = 5.0

The same contract is committed as examples/mixamo.animsmith.toml for reference (from a repo checkout, pass it with --config examples/mixamo.animsmith.toml instead of saving a local copy). It pins the profile rather than trusting auto-detection, declares the clip a loop (arming the loop checks) and gives XZ movement to gameplay (arming in-place, judged on the Hips track per the callout above). Every key, glob pattern, and severity override is documented in the configuration reference.

Lint against the contract:

$ animsmith lint walking.glb
walking.glb:
  coverage[bind-pose] first_frame_rest_delta 'mixamo.com': insufficient_rotation_evidence: ...
0 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s)   # exits 0

And this is what a violation looks like — the same contract against a copy whose cycle was cut short, the classic popped loop:

$ animsmith lint walking-popped.glb
walking-popped.glb:
  error[loop-closure] clip 'mixamo.com' bone 'mixamorig:RightFoot' @1.000s:
    loop does not close in position (measured 0.1581, expected 0.0100)
  error[loop-seam] clip 'mixamo.com' @1.000s: loop seam pops: wrap discontinuity
    is 6.82× the neighbouring in-clip step (cap 1.60) — the clip does not
    close its cycle (measured 6.8152, expected 1.6000)
  error[loop-seam-vel] clip 'mixamo.com' bone 'mixamorig:RightFoot' @1.000s:
    loop velocity changes at the seam (measured 0.7972, expected 0.1000)
3 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s)   # exits 1

The contract also catches downloading the wrong variant. The real download above is Mixamo's default Walking — the traveling one — and under this In Place contract it fails exactly as it should:

$ animsmith lint walking.glb                      # real download
walking.glb:
  error[in-place] clip 'mixamo.com': declared in-place but the root travels
    at 1.79 m/s — the character will glide at runtime (measured 1.7857,
    expected 0.0000)
  ...

For a root-motion download like that one, give XZ ownership to the animation and add a speed pin, using the speed_mps that measure reported as the starting value:

[clips."mixamo.com"]
loop = true
movement_owner_xz = "animation"
speed_mps = { value = 1.8, tolerance = 0.15 }

Older configs may spell XZ gameplay/animation ownership as in_place = true or false. That alias remains supported, but one selector entry cannot declare both in_place and movement_owner_xz.

Under that contract the real download passes the loop and speed pins and surfaces genuine content findings instead (abridged):

$ animsmith lint walking.glb                      # real download
walking.glb:
  warning[foot-slide] clip 'mixamo.com' bone 'mixamorig:LeftFoot' @0.333s:
    left foot skates during stance: speed deviates 0.52 m/s from the
    expected 0.00 m/s (cap 0.30) — foot plants will slip at runtime ...
  warning[foot-slide] clip 'mixamo.com' bone 'mixamorig:RightFoot' @0.833s: ...
0 error(s), 3 warning(s), 103 note(s), 0 coverage gap(s)   # exits 0

Whether that stance drift is acceptable for your game is exactly the kind of decision the contract records — tighten the cap, accept it with --allow, or send the clip back for cleanup (see feet slide within one clip).

From here the contract grows with your project: rename clips as you import more animations, add [clips."run_*"] globs, speed pins, and gait_groups blend rings as the locomotion set fills out — the cookbook's project contract section and the worked character.animsmith.toml show that full shape. Commit animsmith.toml next to your assets and every bare animsmith lint — local or in CI — enforces it.

Where to go next

What game-ready means explains every failure mode these checks catch; everything else — the cookbook, the CLI reference, embedding — is routed from all pages.

Symptoms

Start from what you see in the engine, not from a check id. One row per symptom: what it looks like, the checks that measure it, the repair, the config surface, who owns the fix, and the page that walks it. Each page opens with the runtime symptom, shows what AnimSmith measured on a synthetic clip, and keeps the precise contract one click down; the built-in check reference and the configuration reference remain the exact authorities.

SymptomCheck(s)Repair / transformConfig surfaceWho fixes itPage
A joint snaps the long way round, or the pose turns to noisenan, quat-norm, quat-flip, time-monotonicfix (quat repairs, lossless)[checks.<id>] severityAnimSmith repairs the representation; the DCC owns non-finite dataThe pose flickers, spins, or explodes
A limb stops before the rest of the body, or the clip is a frame shortduration-sanity, fpstransform --slice, --hold-extend[clips.<name>] duration_s, fpsPipeline edit or re-exportThe clip is the wrong length or freezes at the end
The cycle jumps or hitches every time it wrapsduplicate-loop-endpoint, loop-closure, loop-seam-vel, loop-seam-rot, loop-seamtransform --drop-duplicate-loop-endpoint for a strict duplicated endpoint; otherwise re-author the endpoint pose or tangents[clips.<name>] loop = true, [checks.loop-closure], [checks.loop-seam-vel], [checks.loop-seam-rot]Artist in the DCC; loop policy is a project decisionThe loop pops
The character slides across the floor, or runs without movingin-place, root-motion-speedre-export; measure for ground truth[clips.<name>] movement_owner_xz, speed_mpsGameplay decides ownership; artist re-exportsThe character glides or runs in place
Feet skate, pop or desync when the runtime blends two clips — including travel speed or foot slide that changes by direction, and a same-time pair that looks mirrored or swaps footfall timinggait-group, sync-group, time-complementtransform --gait-anchor for explicitly in-place cycles; runtime phase offsets for root motion[gait_groups.<name>], [sync_groups.<name>]Technical animator declares the ringFeet skate when clips blend; narrower: members travel at different speeds, a pair is time-complementary
A planted foot drifts during stance inside one clipfoot-slidere-author in the DCC[clips.<name>] speed_mpsArtist; contact cleanup is DCC workFeet slide within a clip
An arm hangs in a T-pose, or a bone never movesmissing-bones, required-bones, frozen-bone, bind-posere-export[clips.<name>] animates_bones, [rig] required_bonesArtist repairs the source rigA limb is T-posed, or a bone never moves
Two files disagree about the skeleton, or a pack reuses one clip nameno per-file check; animsmith inspect plus a collection manifest retain identitynone — retain the exact (file, clip) bindingcollection manifest sources, clips, and runtime setsPack owner and the ingesting pipelineFiles disagree about skeleton or clip identity
An attachment, socket or helper imports at the wrong sizerest-world-scaleapply or rebake the unintended source hierarchy scale, then re-export[runtime_nodes] selectors, [checks.rest-world-scale] expected_uniform_scaleArtist or exporter settingsThe file is bloated, or the retargeter chokes
Clips cost far more than the motion in them, or a retargeter chokesconstant-track, scale-keys, non-uniform-scale, opt-in constant-nonunit-scaletransform --prune-constant-tracks after reviewing transition coverage[checks.<id>] severity, [clips.<name>] animates_bonesArtist or exporter settingsThe file is bloated, or the retargeter chokes

Where the repair column says re-export, that is deliberate: AnimSmith rewrites a clip only in ways whose within-clip correctness its own checks can verify. Those are the lossless quaternion repairs and the bounded mechanical edits — slice, hold-extend, in-place gait anchor, duplicate-loop-endpoint removal, constant-track pruning and FBX-to-glTF conversion — while artistic transformation stays DCC work, for the reason in what animsmith is not. A clean run on any row here is also still scoped evidence: runtime integration caveats apply after it, including the sparse transition coverage a blend or transition set is judged on.

The table above carries every registered check id, including the narrower presentations of a symptom that no page of its own owns. The readiness ladder stages what a clean run on any of these pages is evidence for.

Whichever row you follow, capture the current lint --format json result, and when the motion itself is contested, the offline report or a before/after diff. A current, repository-safe diagnostic pair is:

# workflow-exit: 0
$ANIMSMITH --config "$CONFIG_DIR/report-comparison.animsmith.toml" report \
    "$ASSET_DIR/report-comparison-before.glb" \
    --compare-after "$ASSET_DIR/report-comparison-after.glb" \
    --before-clip acceptance-matrix --after-clip acceptance-matrix \
    -o "$WORK_DIR/symptom-comparison.html"

It shows a deliberately defective loop and stance trail; it is diagnostic evidence, not an engine or artistic acceptance result.

Not about the clip

Three runtime problems produce no content finding, so none of them has a row above. Each is answered by reading the run itself and then by the project that consumes the asset.

A loader error or an AnimSmith refusal. Read stderr and the command's exit code, then inspect engine-track-support, engine-addressability, input identity, and coverage facets. Correct a bad path, config, or supported source fact; do not reinterpret a refusal. Engine feature and import settings belong to the engine project. The gate closes on a successful rerun with complete applicable source evidence, plus engine-observed load evidence where the route requires it — for Bevy, the revision-3 lint command in the intake workflow.

A clip exists but cannot be addressed in-engine. Inspect generate addressability source rows, selector and index evidence, the dependency closure, and the engine profile's coverage state. Regenerate an application manifest after a source-order change; application name policy, loaded handles, targets, and graph wiring are engine code. The gate closes when the source inventory is complete and the target project records a resolved runtime asset, selector, target, and playback.

Animations vanish in Bevy with no lint error. No content finding exists, because the loader's bevy_animation feature or its load_animations setting dropped them; the Bevy profile's revision-3 gate predicts exactly that negative outcome. The gate closes in the Bevy project that loads the asset, not in a lint run.

The exact fields, severities, prerequisites, and configuration keys stay authoritative in the built-in check reference and the configuration reference. If evidence identifies a vendor or source delivery defect, return the source and its evidence; if it identifies runtime or artistic uncertainty, keep the project-owned gate open.

The pose flickers, spins, or explodes

A joint snaps the long way round between two keys, a limb jitters for a frame, or the whole character turns into visual noise.

The pose flickers, spins, or explodes

Checks: nan · quat-norm · quat-flip · time-monotonic

Why it happens

Rotation is stored as quaternions, and engines are stricter about the maths than exporters are. A key that is not unit length skews skinning, two adjacent keys on opposite hemispheres make an engine interpolate the long way round, and one non-finite float poisons every pose that interpolates through it. Nothing here is an artistic decision — it is the representation of the rotation, which is why these are the checks that need no configuration at all.

What AnimSmith measures

The synthetic clip below carries one non-unit rotation key and one sign-flipped key on the same bone; everything else is the clean clip.glb. The report names both, with the frame each was judged at.

Both defects are in the representation rather than the pose. AnimSmith normalizes every rotation key it samples, and a quaternion and its negation are the same rotation, so the judged frames below play back as the clean swing — the findings list is what marks the two defective keys. That is the point: an engine that does not normalize, or that slerps without neighborhood correction, is the one that flickers.

Open the interactive report to scrub the exact keys the checks judged.

What the finding looks like

$ animsmith lint examples/assets/clip-dirty.glb
examples/assets/clip-dirty.glb:
  error[quat-norm] clip 'swing' bone 'spine' @0.500s: non-unit rotation key (worst at key 2) (measured 1.0500, expected 1.0000)
  warning[quat-flip] clip 'swing' bone 'spine' @0.750s: 2 hemisphere flip(s) between adjacent rotation keys (first between keys 2 and 3) — engines without neighborhood correction will spin the long way (measured 2.0000)
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: swing): only 1 usable first-frame rotation track(s); at least three are required
1 error(s), 1 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 1

Both defects are lossless to repair, so this is the one symptom family AnimSmith fixes for you. --dry-run reports the pending repairs and writes nothing:

$ animsmith fix examples/assets/clip-dirty.glb --dry-run
  would fix[quat-norm] clip 'swing' bone 'spine': 1 key(s) unit-normalized
1 key(s) would be fixed across 1 track(s) -> no output written
  would fix[quat-flip] clip 'swing' bone 'spine': 1 key(s) hemisphere-normalized
1 key(s) would be fixed across 1 track(s) -> no output written   # exits 1

$ animsmith fix examples/assets/clip-dirty.glb -o fixed.glb
  fixed[quat-norm] clip 'swing' bone 'spine': 1 key(s) unit-normalized
1 key(s) fixed across 1 track(s) -> fixed.glb
  fixed[quat-flip] clip 'swing' bone 'spine': 1 key(s) hemisphere-normalized
1 key(s) fixed across 1 track(s) -> fixed.glb   # exits 0

$ animsmith lint fixed.glb
fixed.glb:
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: swing): only 1 usable first-frame rotation track(s); at least three are required
0 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 0

What AnimSmith can do

The comparison below is that repair, run by the generator that renders this page: the before side is clip-dirty.glb and the after side is the file animsmith fix wrote from it, not a hand-authored clean copy of the clip. Each side prints its own SHA-256 above the pose, which is what pins the after side to that output — the document records both inputs by content identity and carries no path.

Open the interactive comparison and press play beside the shared phase to run both sides together, or tick Overlay after on before to draw the two skeletons in one pane — the before solid, the after dashed over it.

The findings list is what changed: the non-unit key and the hemisphere flip on the before side, and nothing on the after side. The two pose panels play the same swing, for the reason above — both defects were in the representation, and AnimSmith normalizes every rotation key it samples — which is also why diff reports no measurement moved. No rig role resolves on this three-bone chain, so the trajectory and gait panels report their evidence unavailable: the findings and the two identities are what this comparison has to show.

What to do

  1. Non-unit or flipped rotation keys. Run animsmith fix. The repair is byte-surgical: meshes, skins, materials and textures pass through byte-identical, and animsmith diff reports no measurement moved.
  2. A NaN or Inf anywhere. There is no safe automatic repair for a value that carries no information. Find the step that produced it — a divide in a constraint, a bad bake, a corrupted transfer — and re-export.
  3. Non-monotonic or late key times. Fix the retiming or export range in the DCC. A first key that starts late is its own hazard: the engine clamp-holds an unauthored pose for the gap.

Who fixes it: AnimSmith repairs the representation, and the DCC owns the data. fix closes the two quaternion classes; a non-finite value, a broken timeline or a bad bake goes back to the export that produced it. The gate closes when the repaired copy re-lints clean and its diff against the original shows no measurement moved.

Config

These checks are mechanical: they run on every file with no configuration. Configuration only changes how hard they fail — promote the hemisphere warning for a project whose runtime slerps without neighborhood correction:

[checks.quat-flip]
severity = "error"
Precise contract: quaternion representation, non-finite values, key times, and what the repairs preserve

Rotation in a clip is stored as quaternions, and engines are strict about the math even when exporters are not.

  • A non-finite value anywhere poisons everything. A single NaN or Inf in a key time or value poisons interpolation and, in most engines, the whole pose — one bad float turns a character into visual noise. The nan check treats this as an error, always; there is no safe automatic repair for a value that carries no information.
  • Non-unit quaternions skew skinning. Rotation keys must be unit quaternions. Engines renormalize inconsistently (or not at all); a non-unit key skews blend weights and skinning. The quat-norm check catches it, and animsmith fix repairs it losslessly — scaling a finite, non-zero quaternion back to unit length preserves the rotation it represents.
  • Hemisphere flips spin the long way around. A quaternion and its negation represent the same rotation, but interpolation between them does not: adjacent keys on opposite hemispheres (dot < 0) make engines that slerp without neighborhood correction take the long way — a visible 360°-minus-θ spin between two keys. The quat-flip check catches it; animsmith fix repairs it losslessly by negating keys until each track is hemisphere-consistent.
  • Key times must move forward. glTF requires strictly increasing key times, and engines misbehave without them. A first key that starts late is its own hazard: the engine clamp-holds an unauthored pose for the gap. The time-monotonic check covers both.

Workflow: a first CLI gate detects these; repairing an asset walks the fix --dry-run → fix → verify loop. The repairs are byte-surgical: meshes, skins, materials, and textures pass through byte-identical.

The checks' prerequisites, configuration keys and gap semantics are listed with every other check in the built-in check reference.

The clip is the wrong length or freezes at the end

A limb stops dead a fraction of a second before the rest of the body, or a clip that should be one second long plays a frame short in the engine.

The clip is the wrong length or freezes at the end

Checks: duration-sanity · fps

Why it happens

A clip's duration is not one number in the file: it is whatever its longest channel reaches, and an engine clamp-holds every channel that ends earlier. A partial export, a slice that dropped one bone's tail keys, or a retiming step that drifted off the frame grid all leave a file that looks valid and plays wrong. The length a clip is supposed to be is project knowledge, so AnimSmith checks the mechanical part always and the declared part only once you pin it.

What AnimSmith measures

The synthetic walk below has one left-ankle rotation channel that stops at 0.75 s while both translation channels run to 1.0 s. The report's judged frame is the moment the short channel starts being held.

Open the interactive report to scrub the frames after the ankle stops.

What the finding looks like

The spread is mechanical, so no configuration is needed to see it:

$ animsmith lint examples/assets/walk-short-channel.glb
examples/assets/walk-short-channel.glb:
  warning[duration-sanity] clip 'walk_short_channel': channels end at different times (0.750s..1.000s) — shorter channels will be clamp-held (measured 0.2500, expected 0.0000)
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: walk_short_channel): only 1 usable first-frame rotation track(s); at least three are required
0 error(s), 1 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 0

The other half of this symptom — a clip whose channels agree but whose length no longer matches the manifest — has no mechanical signature at all. Only a declared duration_s (and fps for the frame grid) turns it into a finding.

What to do

  1. Channels ending at different times. The export dropped part of a channel. Re-export the full range; duration-sanity reports the spread, it does not resample the clip.
  2. A declared length that is missed. Pin duration_s for the clip and let the check report the measured and expected seconds. The tolerance absorbs harmless exporter rounding, not a wrong authored range.
  3. Keys off the frame grid. Declare fps and fix the retiming step that drifted. Unreal, for example, documents that animations with non-whole end frames do not import correctly.
  4. When the input is deliberately being edited. animsmith transform --slice cuts a window on the frame grid and retimes it to start at zero, and --hold-extend appends a linear hold of the final pose for charge and block poses. See editing a clip.

Who fixes it: the pipeline. A partial export or a drifted retime is repaired where it was produced — a re-export, or an explicit transform when the cut itself is the intended edit. The gate closes when the clip's channels agree, its declared duration and frame grid are met, and the engine plays the whole range.

Config

[clips.walk_short_channel]
# The authored range. Linting reports a missed pin; it never resamples.
duration_s = { value = 1.0, tolerance = 0.02 }
# The grid the clip was authored on, so `fps` can check that its keys stay
# on that grid and that it spans a whole number of frames.
fps = 32.0
Precise contract: channel ends, declared durations, the frame grid, and the mechanical edits
  • Channels that end at different times mean a partial export. When one bone's track is shorter than the clip, the engine clamp-holds the shorter channel — a limb freezes while the rest of the body keeps moving. The duration-sanity check flags degenerate durations and mismatched channel ends.
  • A valid-looking clip can still be one frame too short. Export slicing, inclusive-vs-exclusive frame ranges, and endpoint removal can produce a positive duration whose channels agree but which no longer matches the gameplay or animation manifest. Declare duration_s = { value = 1.033, tolerance = 0.02 } for the clip and duration-sanity reports the measured and expected seconds when that pin is missed. The value must be finite and positive, and the tolerance finite and non-negative; invalid pins are errors instead of silently disabling the contract. The tolerance absorbs harmless exporter rounding. Linting only reports the mismatch; it does not repair or resample the clip. Re-export when the authored range is wrong, or use the explicit slice/hold transforms below when the source is intentionally being edited.
  • Keys off the frame grid mean a retiming step drifted. A clip with a declared frame rate should keep its keys on that rate's time grid and span a whole number of frames. Off-grid keys mean a resample or retiming step drifted; a fractional frame count means a slice cut mid-frame — and engines care: Unreal, for example, documents that animations with non-whole end frames do not import correctly. The fps check verifies both once the config declares a rate.

When the wrong length is the input problem — a capture with garbage at the head, a one-shot that should hold its final pose — the transform command does the mechanical edit: --slice cuts a window on the frame grid and retimes it to start at zero, and --hold-extend appends a linear hold of the final pose (charge and block poses). See editing a clip.

The checks' prerequisites, configuration keys and gap semantics are listed with every other check in the built-in check reference.

The loop pops

Every time the cycle wraps, the character jumps or hitches for one frame.

The loop pops

Checks: loop-closure · loop-seam-vel · loop-seam-rot · loop-seam · duplicate-loop-endpoint

Why it happens

A looping clip needs the pose and the motion to be continuous across the wrap. An export range that ends a frame early, an unbaked cycle modifier, or copied endpoint keys with mismatched tangents all leave a gap that the engine crosses in a single frame. A pose gap is a visible jump; a velocity gap is a hitch or pulse once per cycle.

What AnimSmith measures

The synthetic walk below was cut a quarter-cycle short. Foot height relative to the hips no longer returns to its first-frame value, so the loop cannot close.

Cut short: walk-dirty.glbClosed cycle: walk.glb
walk — foot height relative to hipsL footR footL−R (right axis)-0.95 m-1.05 m0.10 m-0.10 mframe 0frame 32walk — foot height relative to hipsL footR footL−R (right axis)-0.95 m-1.05 m0.10 m-0.10 mframe 0frame 32

walk.glb is the clean control this fixture was cut from, not a repair of walk-dirty.glb: it is there to show what the closed contract looks like. AnimSmith does not re-author the endpoint pose.

Open the interactive report to scrub the exact frames the checks judged; the findings list on the right jumps the viewer to the seam.

What the finding looks like

$ animsmith --config examples/walk.animsmith.toml lint examples/assets/walk-dirty.glb
examples/assets/walk-dirty.glb:
  error[loop-closure] clip 'walk' bone 'foot_r' @1.000s: loop does not close in position: bone 'foot_r' is 0.1581 m from its first-frame model-space position (cap 0.0100 m) (measured 0.1581, expected 0.0100)
  error[loop-seam] clip 'walk' @1.000s: loop seam pops: wrap discontinuity is 6.82× the neighbouring in-clip step (cap 1.60) — the clip does not close its cycle (measured 6.8152, expected 1.6000)
  error[loop-seam-vel] clip 'walk' bone 'foot_r' @1.000s: loop velocity changes at the seam: bone 'foot_r' differs by 0.7972 m/s between the incoming and outgoing model-space velocities (cap 0.1000 m/s) (measured 0.7972, expected 0.1000)
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: walk): only 0 usable first-frame rotation track(s); at least three are required
3 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 1

The closed cycle passes the same contract. What remains is the fourth, narrower warning: this export repeats frame 0 at the authored endpoint.

$ animsmith --config examples/walk.animsmith.toml lint examples/assets/walk.glb
examples/assets/walk.glb:
  warning[duplicate-loop-endpoint] clip 'walk' @1.000s: declared loop repeats its first pose at the authored endpoint: 1 redundant closing key(s) per track; ...
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: walk): only 0 usable first-frame rotation track(s); at least three are required
0 error(s), 1 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 0

What to do

  1. Nonzero position or rotation delta. Fix the export range or the endpoint pose in the DCC, then re-export. AnimSmith does not re-author poses.
  2. Zero pose delta, high velocity delta. Match the endpoint tangents on both sides of the seam in the DCC.
  3. Only a duplicated final frame. When duplicate-loop-endpoint warns, animsmith transform --drop-duplicate-loop-endpoint turns the clip into an open cycle losslessly. Read the loop contract against loop_endpoint_mode afterwards: loop-closure compares a repeated final sample with frame 0, so an open cycle intentionally does not keep it green.

Who fixes it: the artist, in the DCC. Only a strict redundant endpoint can be removed mechanically; pose, tangent and contact repair is DCC work, and loop policy (which clips loop, how strict the caps are) is a project decision recorded in the config. The gate closes when the declared closure and seam checks pass and the loop plays cleanly in the target engine's graph.

Config

Declare the clip as a loop; the caps below are the defaults, shown so you can tighten or loosen them per project.

[clips.walk]
loop = true

[checks.loop-closure]
max_position_delta_m = 0.01
max_rotation_delta_deg = 1.0

[checks.loop-seam-vel]
max_velocity_delta_mps = 0.1

[checks.loop-seam-rot]
max_angular_velocity_delta_degps = 5.0
Precise contract: the four seam checks, model space, endpoint modes, root-motion loops and the diagnostic split

A looping clip needs both pose and motion continuity at the wrap. A pose offset is a C0 discontinuity: the runtime jumps when it returns to frame 0. A matching pose with mismatched incoming and outgoing velocity is a C1 discontinuity: it reaches the right point but changes direction or speed abruptly, producing a hitch or pulse once per cycle. Unity's looping-clip guide shows the same artist-facing start/end match problem and the special treatment root-motion axes may need.

Animsmith separates four questions:

  • loop-closure finds the largest last-to-first model-space position and shortest-path model-space rotation delta across all skeleton bones. The default caps are 0.01 m and 1 degree.
  • loop-seam-vel finds the largest difference between the model-space linear velocity entering the last sample and leaving frame 0. Its default cap is 0.1 m/s. A closed triangle-wave trajectory demonstrates the problem: the first and last position are identical, but the bone reverses direction at the wrap.
  • loop-seam-rot finds the largest difference between the shortest-path model-space angular velocities entering the last sample and leaving frame 0. Its default cap is 5 deg/s. This is rotational C1 continuity: the pose can close rotationally (C0) yet still snap in direction or turn rate at the wrap when the two seam-adjacent rotation steps disagree.
  • loop-seam remains the locomotion-specific test. It compares feet relative to hips and normalizes by the neighbouring stride step, so it needs resolved hips/foot roles and deliberately has no result for a stationary clip.

There is a fourth, narrower warning for an export-mode problem rather than a pose-continuity judgment: duplicate-loop-endpoint. Many DCC workflows export an inclusive range or bake a cycle by copying frame 0 again at the final frame. For a declared loop, animsmith recognizes only the mechanically certain subset: every authored channel has one common finite, strictly increasing timeline and valid cardinality; first and final vector components match within 1e-5 and quaternions match within a sign-invariant 1e-4 radians; and the clip has real interior motion. A stationary hold is therefore not mistaken for a cycle. The warning is default-on, needs no rig roles, and is otherwise not applicable unless loop = true is declared.

That duplicate can matter to an engine that loops by advancing over the clip duration: it evaluates or holds the same pose twice before wrapping, which can look like a one-frame hitch. transform --drop-duplicate-loop-endpoint turns only an eligible candidate into an open cycle: it atomically removes the same complete terminal key count from every channel (and cubic-spline key triplets), preserves retained authored data, and re-pins duration. It does not repair a nonclosing/root-travel clip, mismatched timelines, a C1 tangent problem, retargeting damage, or a runtime loop blend. Blender's Cycles F-curve modifier and Unity's looping-clip guide are useful places to inspect the authored range and engine import behavior.

An open cycle intentionally does not keep loop-closure green: that existing inclusive check compares a repeated final sample with frame 0. The loop_endpoint_mode measurement distinguishes strict duplicate endpoints, non-duplicate closing cycles, and non-closing cycles for declared loops.

The first three checks are per-bone and role independent, so idle, guard, block, aim-offset, facial, and prop loops remain testable without a humanoid profile or detectable stride. Model-space is intentional: a parent mismatch can move or rotate many descendants even when their local keys match. JSON measurements retain a stable bone_index plus display bone_name for every row, while findings name only the maximum offending bone for each judged dimension.

Typical causes are an export range ending one frame early, a cycle modifier or procedural controller not being baked, copied endpoint keys whose Bezier tangents do not match, retargeting or resampling that changes one endpoint, and an engine importer using a different clip range from the DCC. A useful diagnostic split is:

  • nonzero position/rotation delta: repair the endpoint pose or clip range;
  • zero pose delta but high velocity delta: repair the endpoint tangents or the seam-adjacent keys;
  • closed rotational pose but high angular-velocity delta: repair the endpoint rotation tangents or the seam-adjacent rotation keys using the intended shortest turn;
  • many descendants reporting the same displacement: inspect their first mismatching ancestor;
  • only loop-seam failing: inspect locomotion phase and foot/hips-relative motion rather than a stationary pose loop.

The normal fix is in the DCC: select the intended complete cycle, bake procedural motion, make the last pose match the first where appropriate, match the derivative on both sides, then re-export and rerun animsmith measure and animsmith lint. Blender's Cycles F-curve modifier can preview repeated curves, but the evaluated endpoint and tangents still need to survive baking and export. Engine-side loop-pose blending can hide a small mismatch at runtime; it does not make the source measurement close.

Root-motion locomotion is the important exception. Intentional horizontal root travel does not return to its starting model-space position, and every child inherits that travel. Tune the global max_position_delta_m, or the clip-level max_loop_position_delta_m, to the contract; alternatively disable loop-closure for such a pipeline. Keep using loop-seam for feet-relative locomotion. loop-seam-vel and loop-seam-rot can still validate constant inherited linear and angular motion respectively.

When one project contains both stationary idles and root-motion locomotion, keep the global [checks.loop-closure], [checks.loop-seam-vel], and [checks.loop-seam-rot] caps strict, then put a finite, non-negative max_loop_position_delta_m, max_loop_rotation_delta_deg, max_loop_velocity_delta_mps, or max_loop_angular_velocity_delta_degps on the relevant [clips."run_*"] family. A [clips.run_forward] entry wins over matching globs for only the fields it supplies, so a one-off authored clip need not copy every inherited value. This is a contract choice, not a repair: it does not make a discontinuous source clip smooth.

There is no general automatic repair. transform --gait-anchor can rotate an explicitly in-place locomotion cycle in time to choose a better stride cut; it refuses accumulating root translation or yaw. Every nonconstant selected Root/Hips trajectory channel must contain exactly one key at each declared whole-frame sample; sparse, differently framed, duplicate-time, or off-grid evidence refuses so the phase shift cannot synthesize values at omitted frames. Duplicate (bone, property) channels refuse too. The safety grid refuses before allocation when declared frames × skeleton bones, declared frames × tracks, or maximum authored keys × skeleton bones exceeds 1,000,000 samples. It does not rewrite arbitrary bone endpoint poses or tangents. Angular-velocity C1 continuity, acceleration/jerk continuity, root-motion extraction policy, and runtime blend settings remain out of scope.

All four checks are judged only on clips declared loop = true — whether a clip is intended to loop is project knowledge — while raw measurements are always available for measurable clips through animsmith measure. Workflow: a project contract config shows the same walk cycle passing clean and failing with a popped seam, and why an undeclared loop is reported clean.

The character glides or runs in place

The character slides across the floor without its feet driving the motion, or it pumps its legs and never leaves the spot.

The character glides or runs in place

Checks: in-place · root-motion-speed

Why it happens

Exactly one thing should move the character horizontally: the controller, or the clip's own root motion. Both is a glide; neither is running in place. The file cannot tell you which was intended — a treadmill cycle and a travelling cycle are both valid glTF — so this symptom only becomes checkable once the project declares who owns each movement component.

What AnimSmith measures

The two synthetic cycles below are the same walk. One keeps its root under the character; the other carries the hips 1.2 m forward over the same second. The root-path figure is measured from the file, so a declared contract can be held against it.

In place: walk.glbTravelling: walk-travel.glb
walk — root path (top-down)rootstartendroot stays at the originX 0.00…0.00 mZ 0.00…0.00 mwalk_travel — root path (top-down)rootstartendX 0.00…0.00 mZ 0.00…1.20 m

Open the interactive report to read the measured trajectory beside the finding.

What the finding looks like

Declare that gameplay owns horizontal travel and the baked 1.2 m becomes a glide:

$ animsmith --config examples/walk-travel-in-place.animsmith.toml lint examples/assets/walk-travel.glb
examples/assets/walk-travel.glb:
  error[in-place] clip 'walk_travel': declared in-place but the root travels at 1.20 m/s — the character will glide at runtime (measured 1.2000, expected 0.0000)
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: walk_travel): only 0 usable first-frame rotation track(s); at least three are required
1 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 1

The same file, the same measured 1.2 m/s. Declare that the animation owns the travel and pin the speed the controller was built around, and the finding changes to a stale pin:

$ animsmith --config examples/walk-travel-root-motion.animsmith.toml lint examples/assets/walk-travel.glb
examples/assets/walk-travel.glb:
  error[root-motion-speed] clip 'walk_travel': measured root-motion speed 1.20 m/s disagrees with the declared 1.00 ± 0.10 m/s — playback scaled by this pin will slide or moonwalk (measured 1.2000, expected 1.0000)
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: walk_travel): only 0 usable first-frame rotation track(s); at least three are required
1 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 1

With no declaration at all the same file is silent. The measurement is always available; the contract is what makes it a finding.

What to do

  1. Decide ownership per component, once. XZ, Y and yaw are independent. Declare each as "gameplay" (the controller owns it) or "animation" (extracted root motion owns it), and configure the importer and controller to match.
  2. Get the ground truth before pinning a speed. Run animsmith measure and read the measured horizontal root speed; a pin copied from a design document is the usual cause of a stale root-motion-speed.
  3. Re-export when the file disagrees with the decision. AnimSmith does not convert root motion to in-place motion, and transform --gait-anchor refuses accumulating root translation precisely so it cannot be misused for that.

Who fixes it: gameplay decides ownership and the artist re-exports. AnimSmith reports the disagreement between the declaration and the measurement, and has no automatic movement-policy repair. The gate closes when the declaration and the measurements agree and an engine trial proves exactly one — not zero, not two — movement producer per component.

Config

[rig]
# `in-place` and `root-motion-speed` need a resolvable root; both fall back
# to the hips role when the rig has no dedicated root bone.
profile = "auto"

# A treadmill cycle: the controller owns horizontal travel.
[clips.walk_travel]
movement_owner_xz = "gameplay"

# Or a travelling cycle, pinned to the speed the controller expects:
#
# [clips.walk_travel]
# movement_owner_xz = "animation"
# speed_mps = { value = 1.2, tolerance = 0.1 }
Precise contract: movement ownership, declared speed, and the root-role fallback

Locomotion clips carry a travel contract between the asset and the runtime, and nothing inside the file can verify it alone.

  • Movement ownership. Declare XZ, Y, and yaw independently as "gameplay" (the entity/controller owns that component) or "animation" (extracted root motion owns it). An in-place/treadmill clip normally uses XZ gameplay ownership; a travelling root-motion clip uses XZ animation ownership. The in-place check compares only declared XZ ownership against measured horizontal root motion. Missing axes remain unspecified and are never inferred from another axis, a filename, or measured magnitude.
  • Declared speed drift. Runtimes scale playback by a clip's declared locomotion speed to keep foot plants locked to world velocity; a stale speed pin plays the clip visibly too fast or too slow. The root-motion-speed check compares the declared speed_mps against the measured horizontal root displacement. Use animsmith measure to obtain the ground-truth number before pinning it.

Both checks need a resolvable root: they use the rig profile's root role, falling back to the hips role when no dedicated root bone exists. That fallback matters in practice — the built-in mixamo profile resolves mixamorig:* bone names but has no root role (Mixamo rigs have no dedicated root bone), so root-motion checks on Mixamo assets judge the hips track.

The legacy in_place boolean remains an XZ-only input alias (true is gameplay, false is animation); one selector entry cannot declare both spellings. The checks' prerequisites, configuration keys and gap semantics are listed with every other check in the built-in check reference.

Feet skate when clips blend

Each locomotion clip looks clean on its own, but the moment the runtime crossfades between two of them the feet skate, stutter or pop.

Feet skate when clips blend

Checks: gait-group · sync-group · time-complement

Why it happens

A directional set — run forward, back, left, right — is blended at runtime, and a blend is only seamless when every member is at the same point in its stride. If one cycle's left foot plants at the top of the loop and another's plants a quarter cycle later, every mix of the two puts the feet somewhere neither clip authored. Nothing in a single clip is wrong; the incompatibility only exists between them, which is why it survives per-clip review.

What AnimSmith measures

The synthetic ring below holds four clips built from one analytic gait, with run_left entered a quarter of the way into that gait. The stride anchor is measured from the left-minus-right foot height relative to the hips, so the shift is visible as the same curve starting at a different point.

run-ring — L−R foot height by stride phaserun_forwardrun_backwardrun_leftrun_right0.10 m-0.10 mphase 0phase 1

The report draws one figure per declared gait group. Every member is on the same normalized source-phase axis and none of them is shifted onto another: aligning them would draw one curve four times and hide the very thing the group is checked for. run_forward, run_backward and run_right share one gait, so their curves coincide and their stride-anchor marks land together at 0.75; run_left runs a quarter cycle earlier and its mark sits at 0.50. The shaded band is the declared 0.15 cap either side of the circular mean of the measured anchors, and run_left is the anchor outside it. No member is nominated as the reference: the check judges the set's spread, so the figure names the members whose anchors fall outside the band and leaves the choice of which one to re-anchor to the project.

Open the interactive report to play the two members together at one phase; the with selector pairs any two members, and both source times are labelled because the pairing is a presentation mapping between two timelines rather than a retime. Enable illustrative blend and move the weight slider to inspect a third pose computed from their sampled local transforms. The source panes keep the judged frames; the third pane is engine-agnostic presentation and adds no measured finding. It does not model your engine's masks, controller, root-motion policy or contact handling. Check the result in the intended runtime; zoom out if an interior pose extends beyond the source-based camera fit.

What the finding looks like

$ animsmith --config examples/run-ring.animsmith.toml lint examples/assets/run-ring.glb
examples/assets/run-ring.glb:
  error[gait-group]: gait group 'run-ring': stride-anchor phases spread by 0.20 cycle (cap 0.15) — directional blends between these clips will skate or pop. Measured: [run_forward=0.75, run_backward=0.75, run_left=0.50, run_right=0.75] (measured 0.1988, expected 0.1500)
  coverage[bind-pose] insufficient_rotation_evidence ×4 (scopes: first_frame_rest_delta; subjects: run_backward, run_forward, run_left, run_right): only 0 usable first-frame rotation track(s); at least three are required
1 error(s), 0 warning(s), 0 note(s), 4 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 1

The finding is group-level, not per-clip: it names the ring, prints every member's measured anchor, and identifies the one that disagrees. Linted without the config the same file is silent — a blend ring is a runtime intent, not something four clips reveal on their own.

What to do

  1. Declare the ring before you debug it. [gait_groups.<name>] is what arms the check. Until the set exists in the config, its coherence is unverified rather than verified-clean.
  2. Re-anchor an explicitly in-place cycle. animsmith transform --gait-anchor rotates a cyclic clip in time so its stride anchor lands at t=0. Selecting it declares the clip in-place, and it refuses accumulating root translation or yaw.
  3. Use runtime phase offsets for root motion. A travelling cycle keeps its trajectory; align the members in the blend graph instead of rewriting the clip.
  4. Check the timing surfaces too. [sync_groups.<name>] holds same-time members to a shared duration, frame grid and endpoint convention, and the optional time_complement diagnostic warns when a pair aligns materially better under reflected time.

Who fixes it: the technical animator declares the ring, and the DCC or the blend graph fixes the disagreement. AnimSmith can re-anchor an eligible in-place cycle and nothing else — mask topology, contact events and graph timing are project work. The gate closes when the declared ring lints clean and a playback capture covers the transitions and contacts on the intended character.

Config

[rig]
# The stride anchor is measured from left-minus-right foot height relative to
# the hips, so the group needs hips plus both foot roles.
profile = "auto"

[gait_groups.run-ring]
clips = ["run_forward", "run_backward", "run_left", "run_right"]
# Cycle fraction: members spread wider than this skate or pop when the runtime
# crossfades between them.
max_gait_phase_spread = 0.15
# Members with less left/right alternation than this are excluded as noise.
min_lr_amplitude_m = 0.03

[sync_groups.run-ring]
clips = ["run_forward", "run_backward", "run_left", "run_right"]
max_duration_delta_s = 0.001
max_frame_count_delta = 0
max_fps_delta = 0.01

[sync_groups.run-ring.time_complement]
min_reflected_time_advantage = 0.25
min_lr_amplitude_m = 0.03
Precise contract: the stride anchor, gait anchoring's refusals, time-complementary pairs, and per-direction travel speed

A directional locomotion set — run forward, back, left, right — is blended at runtime, and blending is only seamless when every member strides in phase. If one cycle's left foot plants at t=0 and another's at mid-cycle, every blend between them skates the feet.

The gait-group check holds a declared blend ring to a shared gait phase (the stride anchor measured from the left−right foot-height fundamental). Members with too little left/right alternation are excluded from the spread — their phase is noise — and a member whose gait cannot be measured at all is an error, so the group's coherence is never silently unverified.

transform --gait-anchor is the matching repair-by-transform: it rotates a cyclic clip so its stride anchor lands at t=0, aligning the set member by member. Selecting it explicitly declares the clip in-place. AnimSmith verifies the Root role (or Hips fallback) before rewriting and refuses missing/non-finite evidence, horizontal endpoint displacement above 1 cm, or yaw accumulation above 1°. No interior step is subtracted as an allowance. Every nonconstant channel the operation would rotate must contain exactly one key at each declared 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 evidence refuses, as do duplicate (bone, property) channels (including constant channels). Verification samples those exact times and mutation uses an integer key-index permutation; exempt constant-track endpoints cannot influence the period or shift. The whole skeleton, roles, and track shapes are validated before declared frames × skeleton bones, declared frames × tracks, and maximum authored keys × skeleton bones are independently bounded at an inclusive 1,000,000 samples. Yaw uses f64 first/final headings plus counted full-turn crossings. At sample zero it selects the local +Z, +Y, or +X basis axis with the greatest finite horizontal projection, in that tie order, and retains it for the whole proof. This accepts different source-axis conventions without switching axes later to hide yaw; loss of the selected horizontal projection refuses. The calculation avoids segment-count-dependent accumulation error. Four f32 successors at the inclusive 1 cm and 1° caps cover only authored endpoint translation/quaternion quantization. Do not apply it to authored root motion: retain that trajectory, use runtime phase offsets, or use a separately designed trajectory-preserving operation. Gait anchoring does not convert root motion to in-place motion.

A blend pair is time-complementary

A pair can be individually clean yet unsuitable for a runtime that samples both clips at the same normalized time. One clip's left/right gait signal may align much better with the other at one minus normalized cycle time than at the same cycle time; blending them together then mixes different stride moments. Unity's Blend Tree guidance similarly calls for blended movements and foot contacts to occur at matching normalized times, while Godot documents explicit cyclic sync modes for keeping blend-space phases aligned.

time-complement compares every unordered pair using the existing left-minus-right foot-height fundamental. It reports same-time and reflected-time similarity on [0, 1] (higher is closer) and warns only when the reflected score wins by more than the configured advantage. An exact-zero left-minus-right swing has no phase subject even when the configured floor is zero. Positive signals below the amplitude floor are coverage gaps, not findings: near-idle or noisy motion does not carry enough phase evidence to classify.

This warning belongs to the declared same-time/absolute-sync contract, not to either animation in isolation. For a declared in-place ring that satisfies the V1 contact-topology, root-trajectory, track, and proof constraints, collection transform-foot-cycle can retime every member to one explicit reference member's contact phases and publish the independently proved generation. Unsupported rings remain unchanged and need aligned contacts from the DCC, explicit contact/phase markers, or a phase-remap strategy in the runtime. animsmith does not reverse clips, choose the runtime strategy, rewrite root motion, or claim that full-body motion is identical under time reflection.

Directional blend members travel at different speeds

Equal cycle duration does not imply equal travel. If two root-motion members have the same duration but different measured horizontal speeds, they cover different distances per cycle: their authored stride lengths differ. A diagonal faster than forward may be intentional, but a controller that assigns one gait-wide speed or normalizes diagonal input can then produce visibly faster diagonal travel or direction-dependent foot sliding.

Compare animsmith measure speed results across every declared directional member and record the project's movement policy. Valid policies include preserving per-direction authored velocities, scaling controller motion or playback per member, or deliberately accepting the variation. If the project requires uniform authored travel, re-time or re-author in the DCC and recheck contacts, phase, and loop seams. Gait anchoring changes phase; it does not make stride lengths coherent and must not be used to rewrite accumulating root trajectories.

Speed variation alone is not a defect because the intended controller policy does not live in the file. AnimSmith currently checks a clip against a declared speed_mps but does not compare a runtime set against a declared cross-member speed/stride policy; #411 tracks that evidence gap.

The checks' prerequisites, configuration keys and gap semantics are listed with every other check in the built-in check reference.

Feet slide within a clip

A planted foot drifts along the ground during stance, so the character looks like it is skating.

Feet slide within a clip

Checks: foot-slide

Why it happens

During stance the foot must move consistently with the clip's declared travel: at speed_mps relative to the character for an in-place clip, or planted in the world for a root-motion clip. Retargeting, resampling, a wrong speed pin, or hand-keyed contacts break that relationship, and runtime IK and blend band-aids exist to hide the result.

What AnimSmith measures

The synthetic gait below has a left foot that skates during stance. The report samples stance intervals with the run's contact threshold and shades them, so the slide is visible as foot motion inside a shaded interval.

Open the interactive report to scrub the exact frames the checks judged; it opens on the first of the two foot-slide findings. This clip declares a loop and a speed_mps pin, so the gait panel's caption says its curves should end where they began and that the shaded bands are the stance intervals foot-slide judged: a foot that moves horizontally inside its band is the slide.

What the finding looks like

$ animsmith --config examples/report-comparison.animsmith.toml lint examples/assets/report-comparison-before.glb
examples/assets/report-comparison-before.glb:
  error[loop-closure] clip 'acceptance-matrix' bone 'left_foot' @1.000s: loop does not close in position: bone 'left_foot' is 0.4000 m from its first-frame model-space position (cap 0.0100 m) (measured 0.4000, expected 0.0100)
  error[loop-seam-vel] clip 'acceptance-matrix' bone 'left_foot' @1.000s: loop velocity changes at the seam: bone 'left_foot' differs by 1.3416 m/s between the incoming and outgoing model-space velocities (cap 0.1000 m/s) (measured 1.3416, expected 0.1000)
  warning[foot-slide] clip 'acceptance-matrix' bone 'left_foot' @0.250s: left foot skates during stance: speed deviates 0.72 m/s from the expected 1.00 m/s (cap 0.30) — foot plants will slip at runtime (measured 0.7200, expected 0.3000)
  warning[foot-slide] clip 'acceptance-matrix' bone 'right_foot' @1.000s: right foot skates during stance: speed deviates 0.88 m/s from the expected 1.00 m/s (cap 0.30) — foot plants will slip at runtime (measured 0.8800, expected 0.3000)
  note[constant-track] clip 'acceptance-matrix' bone 'hand': rotation track has 5 keys but never moves — export bloat
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: acceptance-matrix): only 1 usable first-frame rotation track(s); at least three are required
2 error(s), 2 warning(s), 1 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 1

What the repair looks like

The after clip below is an authored repair: both sides come from this repository's fixture generator, and the after side is the same gait with those defects repaired. It is not the output of an AnimSmith transform, because AnimSmith does not re-author contacts.

Open the interactive comparison to see the two side by side; press play beside the shared phase to run them together, or tick Overlay after on before to put the two skeletons in one pane. The skeleton and the root path are the same on both sides, so what the panels show is the stance repair.

What AnimSmith itself does here is the measuring and one narrow transform: it reports the slide with the stance intervals it judged, and transform --gait-anchor can rotate an eligible in-place cycle's phase to choose a better stride cut. Anchoring changes which frame the cycle starts on, not how fast a planted foot travels, so it does not repair this finding. Contact cleanup stays DCC work.

What to do

  1. Check the speed pin first. Run animsmith measure on the clip and compare the measured horizontal root speed with the declared speed_mps; a stale pin produces a slide finding on a clean clip.
  2. Re-author the contacts in the DCC. Plant the stance foot (or move it at the declared speed for an in-place cycle), then re-export and re-lint.
  3. Do not paper over it with runtime IK before the source is clean; the check is there so the band-aid stays optional.

Who fixes it: the artist. AnimSmith can report the slide and, for an eligible in-place gait, re-anchor the cycle, but contact cleanup and blend timing are DCC and runtime work. The gate closes when the clip re-lints clean under the declared contract and stance behaves in the actual blend.

Config

[rig]
roles = { root = "root", hips = "hips", left_foot = "left_foot", right_foot = "right_foot" }

[clips.acceptance-matrix]
loop = true
speed_mps = { value = 1.0, tolerance = 0.1 }
movement_owner_xz = "gameplay"

[checks.foot-slide]
contact_height_m = 0.03
max_slide_mps = 0.3
Precise contract: stance detection, the declared speed, and why this check is a warning

During stance — the part of the stride where a foot is planted — the foot must move consistently with the clip's declared travel: at speed_mps relative to the character for an in-place clip, or planted in the world for a root-motion clip. Deviation is the skate that runtime IK and blend band-aids exist to hide.

The foot-slide check measures stance-phase foot velocity against the declared speed_mps and the clip's measured horizontal root speed. It is the research-grade check of the catalog: contact detection is heuristic, so it ships as a warning with generous defaults, and is judged only on clips that declare speed_mps.

The check's prerequisites, configuration keys and gap semantics are listed with every other check in the built-in check reference.

A limb is T-posed, or a bone never moves

One arm hangs out sideways while the rest of the body animates, a socket is missing on the target rig, or the whole character deforms as if it were authored on somebody else's skeleton.

A limb is T-posed, or a bone never moves

Checks: missing-bones · required-bones · frozen-bone · bind-pose

Why it happens

A bone can fail in four ways that all look alike in the engine: it is absent from the skeleton, it is present but carries no keys, it carries keys that never move it, or it moves relative to a bind pose that was not this rig's. A presence-only check passes the third case, and a motion-only check passes a socket that is meant to be static. What separates them is a declaration: which bones the clip is supposed to animate, and which must simply exist.

What AnimSmith measures

The synthetic walk below is declared to animate both arms. arm_l is keyed at five identical values, and arm_r never reached the file at all. The report plays back the exact frames the checks judged, so the still arm is visible in the pose grid rather than inferred from the message.

Open the interactive report to scrub the cycle and watch the left arm stay where it started; it opens on the frozen-bone finding that names it.

What the finding looks like

$ animsmith --config examples/walk-frozen-arm.animsmith.toml lint examples/assets/walk-frozen-arm.glb
examples/assets/walk-frozen-arm.glb:
  error[missing-bones] clip 'walk_frozen_arm' bone 'arm_r': required bone does not exist in the skeleton
  error[frozen-bone] clip 'walk_frozen_arm' bone 'arm_l': required bone rotates only 0.00° over the clip (floor 1.00°) — frozen/T-posed limb or a wrong-source slice (measured 0.0000, expected 1.0000)
  note[constant-track] clip 'walk_frozen_arm' bone 'arm_l': rotation track has 5 keys but never moves — export bloat
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: walk_frozen_arm): only 1 usable first-frame rotation track(s); at least three are required
2 error(s), 0 warning(s), 1 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 1

Without the declaration only the mechanical note survives. Nothing in the file says the arms were supposed to move, so nothing in the file can be wrong about it:

$ animsmith lint examples/assets/walk-frozen-arm.glb
examples/assets/walk-frozen-arm.glb:
  note[constant-track] clip 'walk_frozen_arm' bone 'arm_l': rotation track has 5 keys but never moves — export bloat
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: walk_frozen_arm): only 1 usable first-frame rotation track(s); at least three are required
0 error(s), 0 warning(s), 1 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 0

What to do

  1. Declare what the clip must animate. animates_bones turns a silently static limb into a failing gate. It matches bone names, not rig roles, so it needs no profile.
  2. Declare what must merely exist. Sockets, IK targets and mask bones are intentionally static, so they belong in [rig] required_bones rather than in a per-clip motion rule.
  3. Missing bone: repair the source rig. AnimSmith does not create bones, rename an export, or retarget a rig. Fix the export skeleton in the DCC and re-export.
  4. Keyed but frozen: find the slice that dropped it. A masked-out channel, a wrong-source slice, or a bake that never ran leaves keys that go nowhere.
  5. Wide bind deviation: check which rig it was authored against. A clip whose first frame is far from this skeleton's rest pose was almost certainly authored against a different bind and will deform incorrectly when retargeted onto this one.

Who fixes it: the artist repairs the source rig. AnimSmith reports structural absence, declared motion that never happened, and a first frame far from rest; target binding, Avatar/Skeleton setup and masks are engine work. The gate closes when a re-export meets the structural contract and the required bones visibly move on the target character.

Config

# Bones this clip must animate. Both checks match names, not roles.
[clips.walk_frozen_arm]
animates_bones = ["arm_l", "arm_r"]

[rig]
# Bones that must exist even though nothing is expected to move them.
required_bones = ["root", "weapon_socket", "ik_hand_l"]

[checks.frozen-bone]
# Real motion moves required bones tens of degrees; the default floor catches
# a truly static bone without flagging subtle idle sway.
min_rotation_deg = 1.0
Precise contract: the four rig-integrity failures, and what each one refuses to guess

Four related rig-integrity failures, in increasing subtlety:

  • A structural rig bone is absent or ambiguous. Runtime sockets, IK targets, mask bones, and attachment points can be intentionally static, so they do not belong in a per-clip motion rule. Put their exact names in [rig] required_bones = ["weapon_socket", "ik_hand_l"]. required-bones passes a present static bone, errors for a missing name, and refuses to guess if duplicate skeleton names make the declaration ambiguous. It also reports an empty or absent skeleton as unable to meet a nonempty structural contract. This check does not create bones, rename an export, retarget a rig, or validate engine-side socket use: repair the source rig in the DCC and re-export.

  • A declared bone is missing entirely. Bones the clip is declared to animate (via animates_bones in the config) must exist in the skeleton and carry at least one keyframed track. The missing-bones check catches slices that accidentally dropped a channel — leaving a limb static — and exports against the wrong rig.

  • A bone has keys but never moves. A required bone whose rotation never exceeds a floor is frozen: a T-posed limb, a wrong-source slice, or a masked-out channel that a presence-only check would pass. Real motion moves required bones tens of degrees; the frozen-bone check's default 1° floor catches truly static bones without flagging subtle idle sway.

  • The clip was authored against a different bind. A clip whose first frame deviates wildly from the skeleton's rest pose was almost certainly authored against a different bind — wrong seed rig, wrong export skeleton — and will deform incorrectly when retargeted onto this one. Small deviations are normal (few clips start exactly at rest); the bind-pose check fires only on a large mean deviation across the animated bones.

The checks' prerequisites, configuration keys and gap semantics are listed with every other check in the built-in check reference.

Files disagree about skeleton or clip identity

Two deliveries that were supposed to be the same character import as different skeletons, or a report says Take 001 and nobody can tell which of the thirty files in the pack it came from.

Files disagree about skeleton or clip identity

Commands: animsmith inspect · animsmith collection lint

Why it happens

Marketplace packs commonly ship one clip per file and reuse an embedded take name such as Take 001 across every one of them, so a clip's display name is not an identifier. Skeletons drift the same way: two exports of "the same" rig can differ in hierarchy or rest signature and stop being exact-skeleton interchangeable. Both are properties of a set of files, and AnimSmith's ordinary checks resolve clips inside one loaded document — so this symptom is answered by retaining identity, not by a check firing.

What AnimSmith measures

There is no chart here, because the evidence is identity rather than motion. inspect reports the exact skeleton and the embedded clip names of one physical file, and a collection manifest binds each (file, clip) pair to a logical id that survives the display name.

The repository's own collection-boundary fixtures are shaped like the marketplace case: walk-a.gltf and walk-b.gltf are distinct physical files with identical bytes and the same embedded name.

$ animsmith inspect crates/animsmith/testdata/collection-spike/source/walk-a.gltf
crates/animsmith/testdata/collection-spike/source/walk-a.gltf
rig profile: none detected
skeleton: 1 bones
  root
materials: 0
mesh instances: 0
clips: 1
  Take 001: 0.000s, 0 tracks, 0 keys max   # exits 0

$ animsmith inspect crates/animsmith/testdata/collection-spike/source/walk-b.gltf
crates/animsmith/testdata/collection-spike/source/walk-b.gltf
rig profile: none detected
skeleton: 1 bones
  root
materials: 0
mesh instances: 0
clips: 1
  Take 001: 0.000s, 0 tracks, 0 keys max   # exits 0

Two files, one name. A third file in the same set carries Take 001 and Take 002, so even the take index is only meaningful together with its file:

$ animsmith inspect crates/animsmith/testdata/collection-spike/source/multi.gltf
crates/animsmith/testdata/collection-spike/source/multi.gltf
rig profile: none detected
skeleton: 1 bones
  root
materials: 0
mesh instances: 0
clips: 2
  Take 001: 0.000s, 0 tracks, 0 keys max
  Take 002: 0.000s, 0 tracks, 0 keys max   # exits 0

What the evidence looks like

A collection manifest is what turns those rows into stable identities: each source keeps its safe locator and expected digest, and each logical clip names the source plus the exact take index and embedded name it came from.

[[sources]]
key = "walk-a"
path = "walk-a.gltf"
config = "fixture.animsmith.toml"
expected_sha256 = "277f55812602cc560dbb432dede43bb145b3caa6cb90493675442a8f5499f044"

[[clips]]
id = "com.example.collection-spike/locomotion/walk-a"
source = "walk-a"
take_index = 0
take_name = "Take 001"

animsmith collection lint runs every declared source and emits that retained binding as machine-readable evidence — the source digest, the config basis, the observed takes, and the runtime sets that cross file boundaries. The fixture set above is the worked example, and collection contracts is the reference.

What to do

  1. Record the physical file, not the display name. A runtime-set member must preserve the exact source path plus the embedded clip name and index. Normalized display names are not reproducible identifiers.
  2. Reconcile the pack's own list separately. State it as a separate fact when a bundled animation list uses different spelling, casing, or ranges from the files; do not silently rename to make them match.
  3. Do not force a skeleton reference. A copied-avatar or skeleton-reference hierarchy mismatch means the referenced mapping is not evidence for that file. Use a compatible individual asset, or obtain an authoritative re-export.
  4. Keep retargeting a separate decision. An engine humanoid or retarget profile may still make two hierarchies compatible, but only after every required chain maps and the target character's deformation, transitions, masks, sockets and root ownership pass in the intended runtime.

Who fixes it: the pack owner and the pipeline that ingests it. AnimSmith retains identity and reports what each file actually contains; it does not merge, rename, or infer set membership, and it does not retarget. The gate closes when the recorded source-to-target mapping is complete and the target character plays the intended clips with visual evidence.

Config

Identity lives in the collection manifest rather than in animsmith.toml. Every source keeps its own document-local config, so declaring a set never changes what a single file means:

schema = "urn:animsmith:schema:collection-manifest:1"
schema_version = 1
collection_id = "com.example.collection-spike"
input_root = "source"

[[runtime_sets]]
id = "com.example.collection-spike/sets/cross-file-gait"
kind = "gait-group"
members = [
  "com.example.collection-spike/locomotion/walk-a",
  "com.example.collection-spike/locomotion/walk-b",
]
Precise contract: skeleton identity, file-scoped clip identity, and what stays per file

Two collection-level contracts are easy to confuse with single-clip rig health:

  • Skeleton/retarget identity. Different bone hierarchies or rest/bind signatures are not exact-skeleton interchangeable. An engine humanoid or retarget profile may still make them compatible, but only after every required chain maps and target-character deformation, transitions, masks, sockets, and root ownership pass in the intended runtime. A copied-avatar or skeleton-reference hierarchy mismatch means the referenced mapping is not evidence for that file; use a compatible individual asset or obtain an authoritative re-export instead of forcing the reference.
  • File-scoped clip identity. Marketplace packs commonly put one clip in each file while reusing an embedded name such as Take 001. A runtime-set member must then preserve the exact source file/path plus embedded clip name; normalized display names are not reproducible identifiers. Reconcile report members against the retained manifest and state separately when a bundled animation list uses different spelling, casing, or ranges.

AnimSmith's gait and sync groups still resolve clips inside one loaded document. The file-scoped collection identity and cross-file set contract are recorded in DESIGN.md Appendix F and emitted by the explicit collection command. The contact-fragment and transition-family additions are interchange declarations. The strict contact-fragment producer now emits one manifest-witnessed sidecar after reloading that source; it does not make ordinary checks cross-file or infer membership, and it adds no transform or runtime policy. Keep per-file evidence and the collection manifest authoritative; do not merge, rename, or infer set membership merely to make a check run.

The file is bloated, or the retargeter chokes

Clips are far larger than the motion in them justifies, a retargeter or importer mangles a character it should handle, or a weapon parented to a socket imports at the wrong size.

The file is bloated, or the retargeter chokes

Checks: constant-track · scale-keys · non-uniform-scale · rest-world-scale · constant-nonunit-scale

Why it happens

"Key everything" exports and baked control rigs write channels that store hundreds of identical values, and every one of them still costs storage and work in every blend the runtime evaluates. Animated and non-uniform scale come from the same place — a constraint bake, a unit conversion, an unapplied object transform — and are the curves retargeters and physics are least consistent about. None of this is invalid, which is exactly why it accumulates: the file plays, so nobody looks.

What AnimSmith measures

The synthetic walk below stretches its pelvis to 1.2 on Y and back, and carries a five-key weapon_socket translation channel that never moves. The report separates the two facts: temporal scale variation, unequal axes, and redundant data are three different findings about the same file.

Open the interactive report to read the three findings against the frames they were judged at.

What the finding looks like

These checks are mechanical, so they run with no configuration:

$ animsmith lint examples/assets/walk-scaled.glb
examples/assets/walk-scaled.glb:
  warning[scale-keys] clip 'walk_scaled' bone 'pelvis': scale animation present — verify it is intentional; many rigs and retargeters mishandle animated scale
  warning[non-uniform-scale] clip 'walk_scaled' bone 'pelvis' @0.500s: non-uniform scale present — verify it is intentional; many rigs and retargeters mishandle non-uniform scale
  note[constant-track] clip 'walk_scaled' bone 'weapon_socket': translation track has 5 keys but never moves — export bloat
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: walk_scaled): only 0 usable first-frame rotation track(s); at least three are required
0 error(s), 2 warning(s), 1 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 0

The note is the only removable one. transform --prune-constant-tracks names every channel it removes, with its original track index:

$ animsmith transform examples/assets/walk-scaled.glb -o compact.glb --prune-constant-tracks
  constant-track removed 'walk_scaled': track index 3 bone 'weapon_socket' translation Linear 5 key(s)
wrote compact.glb (4 node(s), 1 clip(s), 0 mesh(es) / 0 position(s), 0 material(s))   # exits 0

$ animsmith lint compact.glb
compact.glb:
  warning[scale-keys] clip 'walk_scaled' bone 'pelvis': scale animation present — verify it is intentional; many rigs and retargeters mishandle animated scale
  warning[non-uniform-scale] clip 'walk_scaled' bone 'pelvis' @0.500s: non-uniform scale present — verify it is intentional; many rigs and retargeters mishandle non-uniform scale
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; subjects: walk_scaled): only 0 usable first-frame rotation track(s); at least three are required
0 error(s), 2 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), 0 required-unavailable prediction facet(s)   # exits 0

The two scale warnings survive on purpose. Animated scale is authored content, not bloat, and removing it is not a mechanical decision.

What to do

  1. Review transition coverage before pruning. Removing a constant channel keeps that clip's standalone pose but makes its (bone, property) coverage sparse. Leave tracks intact when a runtime transition does not explicitly reset the omitted property.
  2. Decide whether the scale motion is intentional. Squash-and-stretch, a visibility trick and a gameplay deformation are all legitimate; a constraint bake, a unit-conversion curve and an unapplied object scale are not. Inspect the curve in the DCC's Graph Editor and remove or rebake only the unwanted channel.
  3. Check what a socket actually inherits. A node's local scale is not its effective scale. Declare the attachment nodes your runtime contract cares about in [runtime_nodes] selectors and let rest-world-scale report the inherited factor.
  4. Fix the source, then verify the export. Re-export, re-lint, and preview in the target engine; the desired end state is that deliberate scale motion remains and accidental curves are gone.

Who fixes it: the artist or the exporter settings. AnimSmith can remove provably constant multi-key tracks and nothing else — it does not judge a non-rest constant pin, rewrite cubic tangents, flatten skeletal scale into geometry, or decide whether an effect is artistically correct. The gate closes when a source recheck plus the target engine's own scale, attachment and visual observation agree.

Config

# Shared attachment/socket/IK policy. Each exact name or `*` glob must resolve
# to exactly one source node; a miss or multiple matches is a coverage gap.
[runtime_nodes]
selectors = ["weapon_socket", "ik_*_target"]

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

# Opt into a unit-scale policy only when the project expects unit scale in
# animation channels.
[checks.constant-nonunit-scale]
severity = "note" # or "warn" / "error" for your project
Precise contract: constant tracks, inherited rest-world scale, the five separated scale facts, and the pruning boundary

Export hygiene problems rarely break playback outright, which is why they accumulate:

  • Constant tracks are export bloat. A multi-key track whose values never move comes from unbaked rig channels, baked controls, or "key everything" exports. It is harmless motion-wise but costs disk space and work in every blend the runtime evaluates — the constant-track check reports it as a note, and the opt-in transform can remove candidates that are constant within the clip. Removal preserves that clip's standalone pose but makes its (bone, property) coverage sparse; leave tracks intact when a runtime transition does not explicitly reset omitted properties.

Attachment nodes and inherited rest-world scale

A node's local scale is not its effective scale. The glTF node hierarchy composes every ancestor transform, so a socket with local scale (1,1,1) can still have rest-world scale 0.01 under a unit-conversion helper. Skinning may look correct because inverse-bind matrices compensate when deforming mesh vertices; an ordinary effect, collision shape, or weapon parented to that socket does not automatically receive the same skinning compensation. It usually inherits the node hierarchy, including the non-unit scale. This is the same parent-scale failure mode described by Unity's Transform documentation, although the exact runtime consequences remain engine-specific.

Declare only source nodes your runtime contract cares about — the config above is that declaration. The shared policy also supplies future engine-unit-scale evaluation; rest-world-scale consumes the same resolved set.

Each exact name or * glob must resolve to one named source node. A miss or multiple matches is reported as a coverage gap, not guessed. A finding carries an ancestor path with source indices and reports either the measured uniform factor or the distinct non-uniform, sheared, reflected, or singular affine class. The tolerance is inclusive for uniform factors. Unavailable/non-finite rest evidence remains a coverage gap.

The older [checks.rest-world-scale].node_selectors field remains a compatibility alias. Do not declare it together with [runtime_nodes].

Fix an unintended result in the source hierarchy or exporter, then rerun lint against the exported asset. AnimSmith does not rescale the file, decide which node names your project uses, infer units from mesh height, or predict a runtime's whole attachment system. Animation-channel scale remains under scale-keys, non-uniform-scale, and constant-nonunit-scale; this check judges the static inherited rest domain only.

Why scale animation deserves its own review

A transform scale is a three-component value (x, y, z). A value of (1, 1, 1) preserves the authored size, a uniform value such as (2, 2, 2) doubles every axis, and a non-uniform value such as (1, 2, 1) stretches one axis. The glTF animation model allows a node's scale to be keyframed with STEP, LINEAR, or CUBICSPLINE interpolation. Blender's keyframe guide explains the artist-facing version of the same idea: keys store property values and interpolation curves determine every value between them.

Scale animation is not automatically invalid. It may be an intentional squash-and-stretch effect, visibility technique, or gameplay deformation. Unreal, for example, explicitly supports non-uniform scale animation and stores scale only for animations that need it. The warning exists because unintentional scale curves are also a common export artifact and because runtime consequences are project- and engine-dependent. Unity documents that non-uniform parent scale can skew rotated children and disagree with some collider shapes.

animsmith separates five facts so a team can make that policy decision without conflating them:

CheckLiteral factTypical sourceWhy review it
scale-keysAt least one scale component changes over time after interpolation.Intentional squash/stretch; constraint or retarget bake; unit-conversion keys; exporter-created curves.It can change proportions, child placement, blending, physics assumptions, and animation storage. Confirm the motion is intentional in the target engine.
non-uniform-scaleX, Y, and Z differ somewhere on the evaluated trajectory.Stretching one bone axis; unapplied object scale; cubic interpolation overshoot between apparently harmless keys.Parent/child hierarchies, normals, colliders, and engine components may treat non-uniform scale differently from uniform scale.
constant-nonunit-scaleA scale channel or single-key pin stays away from (1, 1, 1). Disabled by default.Unit conversion; a deliberately resized character; an unapplied static transform that survived into the rig.Often harmless, sometimes a pipeline-policy violation. Enable it only when the project expects unit scale in animation channels.
rest-world-scaleA selected source node's inherited rest-world affine scale differs from its configured uniform policy. Quiet until node selectors are supplied.Unit-conversion ancestor; non-uniform or reflected helper hierarchy; unapplied object scale.Runtime attachments can inherit this scale even when inverse binds make the skinned mesh look correct.
constant-trackA multi-key track stores repeated values and never changes."Key everything" export, baked controls, or importer-generated constant curves.It is redundant data even when its value is valid. Unity exposes a corresponding importer option to remove constant scale curves.

Examples:

  • Keys (1,1,1) → (1.1,1.1,1.1) → (1,1,1) trigger scale-keys but not non-uniform-scale: the character grows uniformly and returns.
  • A constant (1,1.2,1) channel triggers non-uniform-scale, and triggers constant-nonunit-scale only when that opt-in check is enabled. Multiple repeated keys also trigger constant-track.
  • Dense (1,1,1) keys trigger constant-track, not scale-keys; there is no temporal scale motion.
  • Equal CUBICSPLINE key values can still trigger scale-keys when their tangents move the curve between keys. Inspect the curve, not only the key diamonds.

Opting into a unit-scale policy is the constant-nonunit-scale severity in the config above.

Fix the source, then verify the exported result

For an unintentional finding, inspect scale channels in the DCC's Graph Editor, identify whether the curve belongs to a deform bone, control, helper, or object, and remove or rebake only the unwanted channel. Check exporter options that key all transforms or resample the FBX transform stack. Blender's Apply transforms can move object-level scale into object data before rigging, but its manual explicitly warns that applying an armature object transform does not rewrite pose animation curves or constraints. Do not treat Ctrl-A as a universal fix for an already animated rig.

Re-export, rerun animsmith lint, and preview the result in the target engine. The desired end state depends on intent:

  • deliberate scale motion remains and is accepted by project policy;
  • unnecessary dense keys are removed while the evaluated pose stays the same;
  • accidental scale motion or non-uniformity is removed at its authoring source;
  • a constant non-unit pin remains only when the rig/import contract requires it.

After the constant-track note identifies redundant multi-key data, transform --prune-constant-tracks can remove flat translation, rotation, or scale tracks (vector tolerance 1e-4, sign-invariant rotation tolerance 1e-3 radians). This is useful when a DCC keys every property or bakes controls into dense holds: the resulting clip has the same standalone modeled motion with fewer evaluated channels and less animation data. As above, the sparser (bone, property) coverage can change runtime transition behavior when omitted properties are not explicitly reset. It prints each exact original track index so you can compare the source and result; review transition coverage, then re-lint and preview in the target engine.

The transform refuses candidate tracks on animates_bones targets, when removal changes sampled local TRS or model-space position/rotation, or when removal would empty the clip. Single-key pins, malformed data, and cubic tangents that create motion above tolerance are not candidates and remain unchanged. These cases can carry semantics AnimSmith cannot safely erase. It does not model or remove custom curves, judge a non-rest constant pin, reduce changing keys, rewrite cubic tangents, perform DCC cleanup, flatten skeletal scale into mesh geometry, retarget the clip, or decide whether an effect is artistically correct. Those operations can change deformation and must stay in the DCC or an engine-aware retarget/import pipeline. The checks turn exported facts into a reviewable work order; they are not general animation cleanup.

For artists: from export to handoff

Where you are: Artist export → Contract → Developer intake → CI gate → Engine check

You have exported a clip, or cleaned one up, and somebody downstream has to trust it. This page is the order to do that in: keep the file you exported from, check what you actually shipped, make only the changes a tool can prove, and hand over the evidence with the asset instead of a verbal "it's fine".

It keeps the original immutable, makes mechanical changes reviewable, and leaves artistic approval where it belongs. For the meaning of the stages, start with why AnimSmith and the readiness ladder. When something already looks wrong in the engine, start from the symptoms instead and come back here to hand it off.

Outcome and boundary

AnimSmith can inspect source facts, measure and compare motion, evaluate a declared contract, safely repair two quaternion representations, and perform bounded mechanical transforms. It can also produce an offline HTML report. It does not decide whether a performance, retarget, contact, controller, or visual result is acceptable in a game. A clean lint result is scoped evidence, not a blanket “game-ready” verdict.

Keep these artifacts separate:

ArtifactOwnerRule
Raw delivery or DCC exportauthor, vendor, or capture teamPreserve immutably with provenance and export settings.
Candidate GLB/FBXanimation authorRegenerate or replace only after reviewing evidence.
animsmith.tomlprojectDeclare only facts the project authoritatively owns.
Measurements, lint JSON, diff, and HTML reportreview/build pipelineAttach them to the candidate they describe.

The broader storage split and its ownership rationale are in the pipeline guide.

Work the candidate, not the source

The commands below are executable examples. $ANIMSMITH, $ASSET_DIR, $CONFIG_DIR, and $WORK_DIR are placeholders supplied by your checkout or automation: the repository test binds them to the current binary, checked-in self-authored fixtures, committed configs, and a disposable output directory. The first comment gives the command's expected exit status; an exit of 1 can be expected evidence that a candidate materially differs.

  1. Inventory the untouched baseline. Run inspect and measure before changing the file. Keep the JSON measurement output with the baseline.

    # workflow-exit: 0
    $ANIMSMITH inspect "$ASSET_DIR/report-comparison-before.glb"
    $ANIMSMITH measure --format json "$ASSET_DIR/report-comparison-before.glb"
    
  2. Declare only project facts. Add loop, movement-owner, speed, rig-role, required-bone, and set membership declarations only after the team agrees they are true — who writes what says which of those you can settle alone and which need the game developer. The configuration reference owns every key and precedence rule; the contract-config cookbook is the runnable starting shape.

  3. Lint and report the baseline. Treat a finding as a precise work order: its check id, measured value, subject, and coverage state say what was and was not evaluated. The built-in check reference owns the check contracts and the machine-output reference owns the JSON fields. Use the existing self-authored comparison pair when a reviewer needs a judged frame, skeleton, root/foot trails, or charts.

    # workflow-exit: 0
    $ANIMSMITH --config "$CONFIG_DIR/report-comparison.animsmith.toml" report \
        "$ASSET_DIR/report-comparison-before.glb" \
        --compare-after "$ASSET_DIR/report-comparison-after.glb" \
        --before-clip acceptance-matrix --after-clip acceptance-matrix \
        -o "$WORK_DIR/author-comparison.html"
    
  4. Choose the right remedy. fix can losslessly normalize finite quaternions and correct quaternion hemisphere continuity. transform can make its explicitly bounded slice, hold, gait-anchor, duplicate-endpoint, or constant-track operation. Use the editing cookbook for those operations. Retargeting, pose/contact cleanup, motion redesign, and an intentional scale change remain DCC work; then export a new candidate. Importer settings and controller behavior remain engine/project work. A source delivery defect or missing contractual clip goes back to the vendor/source owner.

    Handed over: the finding as a work order, and a new candidate export back — the immutable source never moves. That round trip is the mocap cleanup gate.

  5. Diff the candidate against the accepted baseline. A binary rewrite is not review evidence. Compare the measured effects and hand off the output with the candidate.

    # workflow-exit: 1
    $ANIMSMITH diff "$ASSET_DIR/report-comparison-before.glb" \
        "$ASSET_DIR/report-comparison-after.glb"
    
  6. Hand off exact evidence. Include input identity, committed config, AnimSmith version, command, lint JSON (including coverage gaps), diff, and any generated report. State the remaining engine, gameplay, and visual gates instead of converting unknowns into a pass.

    Handed over: the candidate plus that evidence bundle, to whoever accepts it — the receiving side runs the same contract, which is what outsourced-asset acceptance makes a two-sided gate rather than a request.

When to stop and route

Evidence saysNext owner
A lossless quaternion repair or documented mechanical transform resolves the finding and the diff is understood.AnimSmith workflow; re-lint and submit the candidate.
Pose, contacts, timing, retargeting, rig mapping, or authored scale must change.DCC/animation author; export a new candidate.
The asset is mechanically sound but importer settings, masks, graph wiring, root-motion application, or target lookup fails.Engine/project integrator; follow the game-developer workflow.
The delivered source cannot satisfy the agreed contract or is incomplete.Vendor/source owner; return it with the evidence, as in outsourced-asset acceptance.
Runtime feel, readability, or style remains disputed after the technical evidence is clean.Gameplay/art direction; perform project-owned visual acceptance.

Next step

Use the symptom index for a symptom-first route, or hand the candidate to the game-developer intake workflow for the engine-observed gates.

For game developers: from pack to engine gate

Where you are: Artist export → Contract → Developer intake → CI gate → Engine check

A pack has landed, and somebody has to say whether it can ship in your game. This page is the order to find out in: inventory what the delivery really contains, pin the engine you are judging it against, settle who moves the character, gather the source evidence, and then close the question in the project itself.

It begins with source evidence and ends with an observed engine gate; AnimSmith predictions are useful prerequisites, not engine execution. When one clip already misbehaves, the symptoms pages route it faster than a full intake pass.

Intake path

  1. Inventory the actual delivery. Keep source files immutable. Inspect every physical file, record logical clip names separately from file names, and classify the real runtime sets: locomotion rings, transitions, attacks, masked overlays, contacts, and interaction chains. A collection manifest can retain that physical-to-logical binding; see collection contracts.

    Handed over: the untouched delivery, with its licence and vendor metadata, into an immutable raw location — marketplace-pack intake is the sorting pass that follows, and raw vs transformed artifact storage is where each artifact lives.

  2. Choose the engine tuple before interpreting an engine check. An engine profile identifies one documented importer boundary. It does not select a skeleton, create a controller, retarget clips, or replace target-engine testing. Use the configuration reference for the profile schema and the maintained engine page for the exact tuple.

  3. Make movement ownership explicit. For each locomotion clip, decide whether gameplay/controller code or extracted animation owns XZ, Y, and yaw movement. This is one of the surfaces only your project can settle; who writes what has the rest. Declare that contract, then inspect in-place, root-motion-speed, and any applicable engine prediction. Do not apply extracted root motion and controller movement twice, or omit both.

  4. Run source evidence before conversion. lint and report establish file and declared-contract evidence; measure exposes the facts used in a disagreement. The built-in check reference and output reference are canonical for those records. Convert or assemble only when the target boundary needs it, and retain the conversion/assembly evidence beside the derived artifact.

  5. Exercise the target project. Load the exact emitted artifact, play each intended clip, test skeleton/retarget mapping, blend/transition timing, masks and contacts, root-motion application, target lookup, and a visual gameplay scene. This is the gate that establishes engine-observed evidence.

    Handed over: the exact emitted artifact and its evidence, to the project that will ship it. Once it is accepted there, every later re-export is judged by CI gating on animation changes against this same contract.

Worked path: Bevy 0.19.0, profile revision 3

The executable examples use the placeholder convention from the animation-author workflow. $FBX_FIXTURE is an authorized self-authored FBX input and $BEVY_CONFIG is the exact TOML fence below, materialized by automation before the command runs.

This path is exact only for bevy / revision 3 / 0.19.0 / gltf-asset-loader. It applies to glTF JSON or GLB, not FBX. Preserve an FBX source, then convert the candidate at the format boundary before applying a Bevy profile:

# workflow-exit: 0
$ANIMSMITH convert "$FBX_FIXTURE" -o "$WORK_DIR/candidate.glb"

The conversion is source-to-GLB evidence, not a Bevy import. Then use the versioned configuration below; it is also committed as examples/bevy-v3.animsmith.toml.

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

[engine.settings]
extension_handler_environment = "bare_empty"
bevy_animation_feature = true
load_animations = true

Run the source-side gate and generate the neutral addressability inventory:

# workflow-exit: 1
$ANIMSMITH --config "$BEVY_CONFIG" lint \
    --select engine-track-support "$ASSET_DIR/walk.glb" --format json
# workflow-exit: 0
$ANIMSMITH generate addressability "$ASSET_DIR/walk.glb"

The profile's engine-track-support prediction can prove only negative loader gate outcomes from the declared feature/settings and bounded source animation/channel inventory. If both gates allow loading, the correct result is required-unavailable runtime survival: AnimSmith does not run Bevy, read back a Bevy import, or prove that an asset, target, graph, or playback survived. Likewise, addressability records source animation indices and canonical selector evidence; it does not prove a live handle resolves in an application. The Bevy profile guide owns the precise rule and coverage boundary.

Close the Bevy gate in the project that will ship the asset:

  • load candidate.glb through the selected application path and record the loaded asset/animation evidence;
  • resolve the expected typed selector and verify the target mapping after the project's own scene root, names, and spawned hierarchy are present;
  • create and exercise the application-owned AnimationPlayer, graph/state transitions, masks, and reset policy;
  • make one explicit decision about each root-motion component, then test it against controller/physics movement so it is neither double-applied nor missing; and
  • capture a visual gameplay trial for clip playback, blending, contacts, attachments, and scale.

Those are project-owned runtime and visual gates. Their recorded observations, not a prediction facet, are the evidence that closes them.

Other engine routes

Use the maintained profile page for each narrower route; do not copy settings from a different project or version.

TargetRoute
Unity 6000.3Unity profile guide: exact Generic revision-2 root-motion prediction or preserved Humanoid route, then Unity import/readback, Avatar, controller, and visual test.
Unreal Engine 5.8Unreal profile guide: FBX tuple, Skeleton and sequence boundary, then import, retarget/graph, root-motion, and visual test.
Godot 4.7Godot profile guide: glTF/GLB scene-import tuple, then scene import, retarget/AnimationTree, root motion, and visual test.
Custom glTF runtimeglTF and generic runtime guide: establish the consumer's own mapping and runtime gates.

Close the intake decision

Keep four results separate: source mechanics, declared contract, engine observation, and gameplay/artistic acceptance. A pack can be usable for a prototype while a blend, retarget, mask/contact, or visual gate remains open; record the owner and the exact missing evidence. For commercial evidence that already exists, use the commercial-pack evaluation guide instead of treating a report as an approval for a different project.

Declaring the contract: who writes what

Where you are: Artist export → Contract → Developer intake → CI gate → Engine check

animsmith.toml is where a team writes down what a clip is supposed to do. Mechanical checks need none of it — they run on any file. Everything else waits for a declaration: with no loop = true, the seam checks have nothing to judge and record themselves as not applicable, and a check that needs a rig role it cannot resolve reports a typed coverage gap instead of guessing.

So the question this page answers is not what the keys mean — the configuration reference is the exhaustive authority for that — but who is allowed to write each one. Some surfaces are facts of the export and an animator can declare them alone. Some are decisions only the game can make, and an animator who guesses at them writes a contract that fails for the wrong reason. The rest have to be agreed once and then committed beside the assets, like code.

The surfaces, and who owns each

"Owner" is who decides the value, not who types it: the file itself is committed once, reviewed like code, and read by everyone.

SurfaceWhat it declaresWho owns the valueWhere the value comes fromMinimal example
[rig] profile, [rig.roles]Which bones are hips, feet, toes and rootArtist alone — a fact of the exported skeletonanimsmith inspect prints the profile it resolved[rig]
profile = "mixamo"
[rig] required_bonesBones that must exist even without keysTogether — the game names them, the export carries themThe engine project's sockets, masks and IK targets[rig]
required_bones = ["weapon_socket"]
[clips."<name>"] loop, max_loop_position_delta_m and its three sibling capsThat the clip is a cycle, and how far its endpoints may driftArtist alone for loop; the caps are a project bar set togetherThe take was authored as a cycle; the caps start at the defaults[clips.walk]
loop = true
[clips."<name>"] duration_s, fpsThe length and frame grid the clip owesTogether — an export setting against a gameplay expectationThe DCC export range, confirmed by animsmith measure[clips.walk]
fps = 30.0
[clips."<name>"] movement_owner_xz, movement_owner_y, movement_owner_yawWhether gameplay or the animation moves the character on each axisGame developer alone — a character-controller decisionThe engine project: whichever is actually driving movement[clips.walk]
movement_owner_xz = "gameplay"
[clips."<name>"] speed_mpsThe travel speed a root-motion clip promisesTogether — the controller states it, the clip holds itanimsmith measure reports the travel; the pin is the agreed value[clips.run]
speed_mps = { value = 3.5, tolerance = 0.2 }
[clips."<name>"] animates_bonesBones this take must actually move, not merely carryTogether — gameplay needs the motion, the rig produces itThe engine project, checked against the DCC export[clips.walk]
animates_bones = ["arm_l"]
[gait_groups."<name>"], [sync_groups."<name>"]Which clips blend as one ring, and which play at the same timeTogether — the technical animator declares the ring and its phase tolerance, the game developer confirms the runtime blend setThe blend graph or state machine that will crossfade them[gait_groups.walk-ring]
clips = ["walk_f", "walk_b"]
max_gait_phase_spread = 0.15
[runtime_nodes] selectorsAttachment nodes whose rest-world scale the runtime relies onTogether — the game names the socket, the export scales itThe engine project's socket names[runtime_nodes]
selectors = ["weapon_socket"]
[engine], [engine.settings]Which importer boundary an engine prediction is about; all four selection keys are required togetherGame developer alone — the shipping project's own tupleThe engine project: version, importer, import settings[engine]
profile = "bevy"
profile_revision = 3
engine_version = "0.19.0"
importer = "gltf-asset-loader"
[engine.settings]
extension_handler_environment = "bare_empty"
bevy_animation_feature = true
load_animations = true
[checks.<id>] severityWhat blocks the gate, what only reports, what is offTogether — it is the project's quality barA lint run shows what promoting a check would cost[checks.quat-flip]
severity = "note"
Collection manifest, [transition_families]Which files hold which clips, and which takes form a familyTogether — the pack owner states what the delivery contains, the game developer binds it to runtime setsThe delivery itself; see collection contractsSee collection contracts

Two rules follow from that table. An artist can go a long way alone: a rig profile, loop, fps and animates_bones already arm the checks that catch a popped seam, a short channel and a frozen arm. But movement_owner_*, speed_mps and the blend groups are the game's decisions — declared without the game developer, they measure the clip against a guess.

Nothing is inferred from omission. Leaving loop out does not declare the clip non-cyclic; it declares nothing, and the loop checks stay not applicable. Ownership is the same: each movement axis is independent, and an omitted one is never assumed.

A complete minimal contract

This is examples/walk.animsmith.toml, the committed contract for the sample walk cycle in first lint in 60 seconds — the smallest file that makes the semantic checks fire:

# A runnable contract for the committed `walk.glb` / `walk-dirty.glb`
# example rigs — the smallest config that makes the semantic checks fire.
#
#   animsmith lint --config examples/walk.animsmith.toml examples/assets/walk.glb
#
# See examples/character.animsmith.toml for the full game-character shape
# (locomotion rings, speed pins, per-check overrides).

[rig]
# The rig's bone names (pelvis / foot_l / foot_r) resolve the built-in
# ue-mannequin profile, so "auto" binds hips + feet with no inline map.
profile = "auto"

[clips.walk]
# Declaring the clip as a loop arms role-free per-bone C0/C1 checks plus the
# locomotion-specific feet-relative-to-hips loop-seam check.
loop = true
# Gameplay owns horizontal travel (the clip is a treadmill cycle), so
# `in-place` expects no measured XZ root travel.
movement_owner_xz = "gameplay"
# A per-clip cap can be stricter than a project-wide loop default. Omitted
# dimensions continue to inherit their global caps.
max_loop_rotation_delta_deg = 0.5

[checks.loop-seam]
# Fail a wrap discontinuity more than 1.6× the in-clip stride step.
max_ratio = 1.6

[checks.loop-closure]
# Model-space endpoint pose caps. Inherited root travel may require a larger
# position cap (or disabling this check) for extracted root-motion clips.
max_position_delta_m = 0.01
max_rotation_delta_deg = 1.0

[checks.loop-seam-vel]
# Fail when incoming/outgoing model-space linear velocity differs by >0.1 m/s.
max_velocity_delta_mps = 0.1

[checks.loop-seam-rot]
# Fail when shortest-path model-space angular velocity differs by >5 deg/s.
max_angular_velocity_delta_degps = 5.0

# The same three caps can live in a `[clips."name_*"]` glob. They do not
# configure angular seam velocity, which is outside these overrides.

Run it against the committed clip and the loop checks pass; run it against the same clip cut a quarter-cycle short and they fail with the measured distance. The examples cookbook walks that pair, and examples/character.animsmith.toml grows the same shape to a full character with rings, speed pins and per-check overrides.

Where to go next

The configuration reference owns every key, default, unit and precedence rule; the built-in check reference owns what each declaration arms. When a declaration turns out to be wrong rather than missing, the symptom index routes what you see in the engine to the page that walks it.

Raw asset to game-ready pipeline

Where you are: Artist export → Contract → Developer intake → CI gate → Engine check

Where animsmith fits in an animation asset pipeline.

This guide is the process-level companion to what game-ready means and the examples cookbook. That page defines the readiness ladder and explains why a check fires; the cookbook gives runnable command transcripts. This page shows how teams place those commands in the larger path from raw source animation to engine-facing, reviewable, CI-gated assets.

Which scenario belongs to which handoff

Every handoff the two reader workflows name lands in one of the scenarios below. Start from the row that matches the artifact currently moving:

HandoffWhat movesScenario
A vendor or marketplace pack arrivesThe untouched delivery, with its licence and vendor metadataMarketplace-pack intake
A finding goes back to the DCC and a new export comes outThe candidate GLB/FBX, beside the immutable source it came fromMocap cleanup gate
An animator hands a candidate to the game developerThe candidate plus its lint JSON, diff and reportOutsourced-asset acceptance
An accepted asset enters the repositoryThe committed asset and the committed animsmith.tomlCI gating on animation changes
Anything above is storedRaw source, work in progress, generated output, config and reportsRaw vs transformed artifact storage

The common shape:

  1. Acquire the source asset.
  2. Preserve the raw source and provenance.
  3. Parse and inspect the file.
  4. Segment long takes into named clips.
  5. Decide root-motion policy per clip.
  6. Retarget, conform, or clean up in DCC tools.
  7. Validate motion semantics.
  8. Optimize the exported data.
  9. Export engine-facing artifacts.
  10. Gate and report changes in CI.

animsmith does not replace DCC work, engine importers, or a team's contract format. It gives the pipeline a repeatable measurement and linting step around them: inspect for inventory, measure for raw numbers, lint for the declared contract, fix for safe repairs, transform for mechanical edits, convert for FBX/glTF handoff, diff for regression checks, and report for review artifacts.

Pipeline map

StagePipeline decisionanimsmith workflow
AcquireIs this source usable and redistributable?Keep the vendor/source asset outside generated outputs; record license and intended rig/profile in project metadata.
Preserve rawWhat must stay immutable?Store raw FBX/glTF/GLB separately from any fix, transform, or convert outputs.
InspectWhat clips, bones, tracks, durations, and rig roles exist?Use inspect for inventory and measure for metrics without judgment.
SegmentWhich gameplay clips come from the take?Use DCC slicing or mechanical transform slicing; declare clip names and frame policy in config.
Root motionWho owns horizontal, vertical, and yaw world motion?Pin [clips.<name>] movement_owner_xz, movement_owner_y, movement_owner_yaw, and speed_mps; verify horizontal intent with lint and measure trajectory evidence with measure.
ConformWhat needs artistic cleanup or retargeting?Use findings as DCC work orders; reserve fix and transform for safe mechanical operations only.
ValidateDoes the clip meet the game contract?Run config-backed lint; use --deny-warnings when warnings must block CI.
OptimizeDid cleanup change what matters?Run lint for constant-track, scale-keys, and non-uniform-scale hygiene, then use diff for metric deltas that catch motion drift.
ExportWhich artifact does the engine consume?Use convert for FBX/glTF handoff; keep transformed GLB and any sidecars separate from raw source.
Gate/reportCan reviewers and automation trust the change?Attach JSON or Markdown lint output to CI, and generate report artifacts for visual review.

Bevy animation gate

When the target is Bevy 0.19.0's stock gltf-asset-loader, the current profile revision 3 adds a deliberately narrow gate after source inspection. engine-track-support binds raw animation rows and independent per-animation channel coverage from the same load to the resolved bevy_animation_feature and load_animations settings. The compiled feature gate wins when the two settings disagree. A complete inventory reports only negative dropped-row outcomes; a complete empty inventory is not applicable.

Partial or unavailable inventory reports exactly one subjectless, unsuppressible required-unavailable facet and no retained-prefix prediction, including bounded N+1 overflow. If both gates allow loading, the result is instead required-unavailable runtime-survival evidence: the pipeline must still exercise the actual Bevy application. This slice has no extension/construct coverage, no positive runtime-survival claim, and no content finding for a dropped row. Current output-v19 carries its immutable V5 provenance unchanged; output-v17 and output-v16 remain historical readers, and rev1/rev2 with output-v15 remain preserved for existing consumers.

Shared contract config

Every scenario below becomes much more useful once the team commits an animsmith.toml next to the asset set. The exact tolerances belong to your project, and the config is where the team records facts such as which clips loop, who owns each clip's XZ/Y/yaw movement, what speed a locomotion clip promises, which bones must move, and which clips form a directional blend set. animsmith checks those facts against the measured asset. It records typed, nonblocking coverage gaps when rig roles cannot be resolved, rather than guessing.

Keep the process guide focused on where the contract is used. For the canonical config shape, use the project contract config cookbook, the committed examples/character.animsmith.toml, and the configuration reference.

Cookbook routing

This guide keeps the pipeline shape here and the runnable transcripts in the command-owned references:

Pipeline needRecipe or reference
Inventory, measurement, lint, and exit-code gatingA first CLI gate
Mechanical slicing, hold extension, or gait anchoringEditing a clip
Shared clip contract and severity policyA project contract config
Conversion and HTML review reportsConverting exports and generating reports
JSON, Markdown, and schema detailsMachine-readable output

Scenario: marketplace-pack intake

Marketplace packs usually arrive as many clips with inconsistent naming, export settings, skeletons, and quality. The intake goal is to sort the pack into "usable now", "needs cleanup", and "reject or replace" before artists build gameplay on top of it — and to record what each verdict is based on.

Intake evidence comes from running animsmith on the actual delivered files. Vendor preview media and partial sample downloads cannot establish raw loop boundaries, complete blend-set compatibility, exact rig and channel behavior, or how a clip will integrate in your runtime. Treat vendor claims as context, keep them distinct from run-backed findings in the intake notes, and preserve open unknowns instead of rounding them to "fine".

Store the downloaded source in a raw, immutable location with its license and vendor metadata. Convert FBX assets into the format your pipeline uses, inspect representative files, measure the pack, then lint the batch before adding stricter project contract expectations. Use the conversion and reporting recipe for conversion and reports, the first CLI gate recipe for inventory and batch linting, and the project contract recipe once clip names and policies settle.

When physical files, repeated take names, and runtime sets are declared in a collection manifest, retain the collection lint JSON and render a local collection dashboard from that exact output. The dashboard is a current-state inventory: it keeps unavailable and incomplete rows visible and can link separately generated offline per-asset reports. Keep those reports and the raw assets outside source control when they contain licensed material. Do not use the dashboard as a quality score or an engine/artistic/gameplay acceptance decision. Its versioned machine authority is urn:animsmith:schema:collection-dashboard:1. Observed physical takes remain visible under their source even when no logical clip declaration binds them; their evidence contributes to the unfiltered summary without creating a logical identity. Source-level findings and subjectless required_prediction_unavailable facets without one exact witness remain counted once at source scope rather than being guessed onto a take or logical clip row.

Record intake as a coverage matrix over the representative files rather than one verdict per file: what was evaluated and clean, what was evaluated and produced findings, and what was not evaluated — because no expectations are declared yet, because a rig role did not resolve (the result carries a typed coverage gap), or because a check was disabled. Not-evaluated cells are neither pass nor fail; they stay visible until the contract grows to cover them or the team accepts them:

FileEvaluated cleanFindingsNot evaluated
run_forward.glbmechanical checks except scale-keysscale-keys warningloop, speed, and gait checks — no expectations declared yet; opt-in scale-channel policy disabled
idle.glbmechanical checks, loop-seam—root-motion-speed — no speed_mps declared

Use the first pass to catch obvious importer hazards: non-finite values, quaternion problems, inconsistent durations, scale keys, constant tracks, missing required bones, loop pops, wrong in-place/root-motion policy, and gait-phase drift in blend sets. When a finding requires artistic judgment, treat it as a DCC cleanup ticket instead of trying to auto-fix motion.

Scenario: mocap cleanup gate

Mocap data often moves through capture, solve, cleanup, retarget, slice, and export steps. The risk is subtle drift: the cleaned clip looks better, but a loop seam, stride phase, speed pin, or required-bone motion changed without anyone noticing.

Measure the raw solve before cleanup. Use transform only for mechanical edits such as slicing, hold extension, or gait anchoring, then measure and lint before and after DCC cleanup with the same config. Compare the cleaned output against the pre-cleanup or last accepted revision with diff. Use the first CLI gate recipe for measurement and linting, the editing recipe for mechanical transform work, and the project contract recipe for semantic checks.

diff is the key review step: it catches meaningful changes in the measurements animators and gameplay programmers care about, rather than asking reviewers to compare binary assets or trust a re-export.

Scenario: outsourced-asset acceptance

Outsourced animation acceptance works best when the vendor receives a machine-readable contract instead of a vague "make it game-ready" request. animsmith can be the acceptance gate both sides run locally.

Recommended contract package:

  • The target skeleton or rig profile expectations.
  • The committed animsmith.toml.
  • A naming table for required clips.
  • Required source and engine-facing artifact paths.
  • The named cookbook recipe or wrapper script the vendor and the receiving team will run.

Use exit codes as the automation boundary:

Exit codeAcceptance meaning
0No failing findings and no required-unavailable engine prediction facets; warnings, notes, and ordinary coverage gaps may remain for review.
1Rejected for a failing finding or any required_prediction_unavailable facet.
2Delivery or command error: missing file, unreadable asset, bad config, or unsupported format.

Exit 0 does not assert that every declared check evaluated. A delivery whose rig the profile cannot resolve records role-dependent work as coverage gaps and still exits 0, so an acceptance gate should also review gaps and the resolved rig roles in the JSON output before accepting. Required-unavailable engine prediction work is distinct from those ordinary gaps: it exits 1 and cannot be suppressed by severity or --allow.

Use the project contract recipe for the shared animsmith.toml, then pair the first CLI gate recipe with the machine-readable output reference for JSON lint output and exit-code gating.

For review, attach the JSON output to the ticket and generate an HTML report for clips with contested findings. The structured output makes it clear whether rejection came from a semantic contract failure such as a popped loop or from a mechanical problem such as bad quaternion keys.

If your acceptance system is already written in Rust, embed the check catalog instead of shelling out. Load with animsmith-gltf or animsmith-fbx, build Config from your own contract format, and map Finding severities to your vendor workflow. The embedding guide shows the API boundary.

Scenario: CI gating on animation changes

CI should answer two questions on every asset change: does the new asset still meet the declared contract, and did any important measurement move?

Run lint on changed clips with the project config, compare changed clips against the previous accepted revision with diff, publish human-readable review artifacts, and gate on process exit rather than by scraping text. Use JSON for automation, Markdown and HTML for humans. Use the first CLI gate recipe for CI lint, the project contract recipe for config-backed checks, the conversion and reporting recipe for report, and the machine-readable output reference for JSON and Markdown outputs.

--deny-warnings is a project policy choice. Many teams start by gating only errors, then promote warnings once the back catalog is clean. Severity overrides in [checks.<id>] let a team make that transition one check at a time without losing visibility.

Scenario: raw vs transformed artifact storage

A reliable pipeline treats raw source and engine-facing outputs as different artifacts with different ownership.

Use this split:

ArtifactOwnershipTypical pathNotes
Raw sourceImmutable vendor/capture/DCC inputassets/raw/vendor_pack/run_forward.fbxKeep license, author, DCC version, export settings, and original units here.
Work-in-progress cleanupAnimator-owned intermediateassets/work/run_forward.blend or assets/work/run_forward.maArtistic changes happen here, outside animsmith.
Mechanical transform outputPipeline-generatedassets/generated/run_forward.glbProduced by convert, fix, or transform; regenerate rather than hand-edit.
Contract configTeam-owned policyassets/animsmith.tomlVersion with the asset set; review changes like code.
Reports and measurementsCI/review artifactsreports/run_forward.html, animsmith-lint.jsonUsually generated per run, not committed unless the project wants baselines.

This separation keeps fix and transform honest. Quaternion repairs, frame slicing, hold extension, gait anchoring, and conversion are mechanical steps whose results can be rechecked. Retargeting, pose edits, contact cleanup, and motion redesign remain DCC work, then re-enter the pipeline through measure, lint, and diff.

Where to go next

What game-ready means explains the runtime symptoms behind each check, and the examples cookbook turns the stages above into runnable commands. Everything else — embedding, the JSON envelope, the dated source research behind this process model — is routed from the all pages.

Engine profiles

  • Unity 6000.3 profile guide — Configure the exact Unity Generic/Humanoid profiles, importer advice, root-motion choices, and scale boundary.
  • Unreal Engine 5.8 profile guide — Map FBX animation, Skeleton, frame, unit, root-motion, and scale concerns to AnimSmith evidence without inventing importer settings.
  • Godot 4.7 profile guide — Plan scene import, retargeting, animation slicing, root scale, and the current profile's explicit prediction boundary.
  • Bevy 0.19.0 profile guide — Generate exact Animation{i} selector evidence and keep runtime loading, graph, target, and scale responsibilities explicit.
  • glTF and generic runtime guide — Use the engine-neutral contract for custom runtimes, glTF units, source identity, scale repair, and downstream validation.

Unity 6000.3 animation profile

Use this page when FBX animation will be imported by Unity 6.3 LTS. AnimSmith has a current Unity Generic root-motion profile plus the preserved Humanoid profile:

Rig pathExact tuple
Genericunity-generic / revision 2 / 6000.3 / fbx-model-importer
Humanoidunity-humanoid / revision 1 / 6000.3 / fbx-model-importer

These are importer contracts, not claims that every Unity 6000.3 project uses the same rig, controller, physics setup, or root-motion policy. Both accept FBX only. Revision 2 is the exact Generic contract used by the engine-root-motion prediction; revision 1 remains readable for historical advice artifacts. An engine profile never changes AnimSmith measurements.

What Unity expects

Unity's Rig tab maps a Generic rig through a chosen root node and a Humanoid rig through an Avatar. Its Animation tab defines clip ranges, loops, and whether rotation, vertical position, and XZ position are baked into the pose or extracted as root motion. A clean source therefore still needs an intentional rig type, Avatar/root selection, clip cuts, movement ownership, and controller setup.

The Model tab documents three separate scale influences: source-file scale, Scale Factor, and Transform scale. The preserved revision-1 advice profile models Convert Units and Bake Axis Conversion; Generic revision 2 deliberately does not claim either setting. It models only the closed animation/root-motion controls listed below, not Scale Factor, Avatar construction, or the imported hierarchy that Unity ultimately creates.

AnimSmith checks and thresholds

The ordinary mechanical and contract-aware catalog runs unchanged under either Unity profile. The most importer-sensitive rows are:

Check idExact default boundaryWhy it matters in Unity
rest-world-scaleselected node factor 1.0, inclusive tolerance 0.0001Finds inherited scale at sockets, IK targets, and attachment nodes before import.
scale-keyscomponent range greater than 1e-4Flags animated scale that may complicate retargeting and attachments.
non-uniform-scalerelative component spread greater than 1e-4Identifies non-uniform scale before Avatar/Transform composition.
constant-nonunit-scaleunit deviation greater than 1e-4; off by defaultOpt in when the project forbids retained constant scale channels.
in-placemeasured XZ root speed at least 0.5 m/s counts as travellingCross-checks declared gameplay-owned versus animation-owned XZ motion.
loop-closure0.01 m position and 1.0° rotationChecks declared loops before Unity's Loop Pose processing.
loop-seam-vel / loop-seam-rot0.1 m/s / 5.0°/sFinds velocity discontinuities that matching endpoint poses can hide.

The current Unity-only check is engine-root-motion, and it is applicable only for the exact Generic revision-2 tuple, an FBX source, and declared movement ownership. engine-addressability remains a Bevy-only rule. The separate generate import-advice contract still projects only settings declared under the historical revision-1 Unity advice profile.

The ownership line is explicit: #267 provides the parent/rest-world/bind scale measurements, #268 provides the selected-node rest-world-scale finding, and #155 provides the Unity importer suggestion document. Static placement baking (#224) and skinned rest/bind reparameterization (#269) are different repair domains.

Configure the exact importer contract

Every applicable revision-2 setting is required; the profile does not invent a default. The exact Generic root-motion tuple is:

[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."*".engine_settings]
root_rotation = "extract"
root_position_y = "bake"
root_position_xz = "extract"

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

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

For Humanoid, select the preserved unity-humanoid revision-1 profile and omit root_motion_source; that setting is not applicable to the Humanoid profile. Generic revision 2 freezes animation_type = "generic", avatar_setup = "create_from_this_model", and import_animation = true; other values are rejected. The three clip-scoped bake or extract choices are required for every actual Generic clip. The example above is a prediction configuration, not a Unity project file.

Root-motion prediction

engine-root-motion compares one declared movement owner with one materialized Unity clip setting for each declared axis: horizontal_xz uses root_position_xz, vertical_y uses root_position_y, and yaw uses root_rotation. bake means baked_into_pose; extract means stored_as_root_motion. Gameplay ownership is compatible with baking, while animation ownership is compatible with extraction. A conflict is an ordinary error finding with a prediction_scope, not a rewritten measurement or an automatic fix.

The check's lifecycle is explicit. A selected, enabled, applicable check emits one available facet per clip/axis when all evidence is present. Its machine result is RootMotionRouting with project_owner, importer_disposition, and compatibility. A clip with no declared axis is not applicable for that axis. Missing raw-path coverage, incomplete project intent, duplicate/overflowed settings, a missing or ambiguous source path, a path that does not identify the resolved Root role, or unavailable axis-specific trajectory evidence emits a required_prediction_unavailable facet instead. Required-unavailable work is not a content finding, cannot be suppressed with --allow, makes the check not_evaluated when all work is unavailable (or partial when mixed), and makes lint exit 1.

Project-intent construction is bounded at each retained source/declaration prefix plus one overflow witness. An unmapped declaration tail is therefore not silently treated as ownerless: NPlusOne intent work makes the exact profile unavailable and produces one atomic summary. A complete, exhaustively observed source/intent inventory with no declared owner remains genuinely not applicable; path evidence is required once declared work exists.

The source path must resolve to the explicitly resolved Root role. This rule does not use the consumer-neutral Hips fallback: a resolved Hips role cannot stand in for a missing Root. Root trajectory measurements may be unavailable for an individual axis, but their numeric magnitude is never an applicability or routing threshold. A zero-travel clip and a travelling clip use the same ownership/setting comparison when their required measurement availability is measured.

Closed raw FBX path evidence

root_motion_source is a case-sensitive, byte-exact relative transform path. The V1 grammar uses / as its only separator and has no escaping or Unicode normalization. Paths must have nonempty segments; leading/trailing /, //, . and .. segments, backslashes, control or Unicode format characters are rejected. A segment is at most 1,024 UTF-8 bytes, a complete path at most 4,096 bytes, and a path at most 256 segments.

The FBX loader projects this path inventory from the same input bytes through a raw-preserving ufbx load. Original source node identities, parent chains, and names are retained; the implicit ufbx root and generated geometry/scale helper nodes remain evidence but cannot satisfy a configured path. Complete coverage is required to prove NoMatch; incomplete or unavailable coverage produces CoverageIncomplete, never a guessed absence. This is raw-source evidence, not a claim that Unity executed the import.

Generate the bounded advice document with the historical advice profile using:

animsmith --config unity.animsmith.toml generate import-advice character.fbx

The result maps the declared values to Unity importer properties. It does not assert that Unity imported successfully or that the imported motion matches the source visually. The root-motion prediction likewise performs no Unity editor execution, imported-asset readback, runtime playback, or engine certification. Repository and CI evidence for this slice uses only self-authored synthetic FBX fixtures; commercial animation packs are not fixtures or uploaded artifacts.

Common failures and fixes

SymptomEvidence to inspectCorrect owner
Character is 100 times too large or smallsource units, rest-world-scale, Unity Scale Factor and Convert UnitsFix source units in the DCC or set the importer intentionally; do not infer a factor from height.
Prop or IK target is offset after importrest-world-scale ancestry plus Unity root compensationRemove a supported compensating skinned scale with scale rest-bind, or fix the hierarchy/import policy.
Character glides or runs in placein-place, root-motion-speed, declared movement owners, three root extraction settingsAlign clip intent, Unity bake/extract choices, and controller ownership.
Loop hitches once per cycleduplicate-loop-endpoint, loop-closure, seam velocity checksCorrect the cut in the DCC or use the exact eligible endpoint transform; verify again in Unity.
Humanoid retarget differs from sourcerequired-bones, bind-pose, scale checks, Unity import messagesRepair mapping/rest pose in the DCC or Avatar configuration; AnimSmith does not certify retarget quality.

Scale and unit workflow

Unity documents one imported world unit as one metre for its physics-oriented scale convention. That does not mean every file should be multiplied until its root Transform reads one: source units, importer conversion, and inherited bone scale are separate domains. An inherited bone scale also multiplies descendant socket, attachment, IK, and collision-anchor offsets, authored animation translations, and measured root travel.

  • If every represented length is wrong, use DCC unit correction or the supported glTF/GLB scale whole-document operation before producing the Unity FBX. FBX whole-document scaling is not supported.
  • If world geometry is already correct but one skinned hierarchy carries a compensating uniform scale, use the explicit scale rest-bind design. FBX input is re-encoded to a proved GLB, so a downstream FBX requirement still needs a deliberate DCC/export step.
  • convert --bake-static-mesh-transforms is for static, unskinned placement. It is not a substitute for skinned rest/bind reparameterization.

Before: a hand socket inherits scale 0.01, so a unit-sized prop or IK offset is scaled unexpectedly. After a successful rest/bind operation: the measured world geometry and joint trajectories are preserved while the selected hierarchy is reparameterized to the declared factor. Still verify Avatar mapping, attachments, collision anchors, root motion, compression, blending, and a player build in Unity. See Scaling safely for the exact write set and proof boundary.

Unreal Engine 5.8 animation profile

Use this page for the exact AnimSmith tuple unreal / revision 1 / 5.8 / fbx-importer. V1 accepts FBX only.

What Unreal expects

Unreal imports skeletal motion as Animation Sequence assets tied to a Skeleton. The FBX animation pipeline documents FBX 2020.2 and requires an existing compatible Skeleton for an animation-only import. The Animation Sequences guide also documents frame ranges, sample rates, custom attributes, bone-name and hierarchy matching, and the requirement that animation end frames be whole.

The frozen profile knows that Unreal uses a left-handed, Z-up basis with +X forward and a centimetre distance unit. It also knows that the importer exposes unit and axis conversion controls. Those facts do not prove the exact imported hierarchy, conversion result, or runtime root-motion policy.

AnimSmith checks and thresholds

The normal check catalog is engine-neutral. Prioritize:

Check idExact default boundaryUnreal-facing use
fpsduration and keys must be within 0.1 frame of the explicitly declared frame gridDetects source timing that disagrees with the project contract; it does not prove Unreal's imported frame range.
loop-closure0.01 m and 1.0°Checks declared loop endpoints before import/resampling.
loop-seam-vel / loop-seam-rot0.1 m/s / 5.0°/sFinds a wrap pulse after pose equality.
scale-keyscomponent range greater than 1e-4Makes imported scale tracks explicit; Unreal may store non-unit scale only when present.
non-uniform-scalerelative spread greater than 1e-4Flags a portability and retargeting risk even though Unreal can import scale animation.
rest-world-scaleselected node factor 1.0 ± 0.0001 inclusiveFinds inherited socket, attachment, IK, and collision-anchor scale.
in-placeXZ speed at least 0.5 m/s counts as travellingCross-checks whether gameplay or animation owns horizontal motion.

For the frozen Unreal revision 1 / 5.8 / fbx-importer profile, engine-clip-boundary evaluates each retained FBX animation stack against the documented whole-end-frame requirement. The FBX adapter retains the parser-resolved absolute KTime end coordinate and exact frame period; the format-neutral core records that evidence without converting it through seconds or decimal FPS. A complete selected range whose end coordinate is off that integer lattice is a finding. Missing, partial, or unavailable timing evidence is a required-unavailable facet, never a guessed frame number. This bounded check does not predict Animation Length, Frame Import Range, resampling, compression, root-motion, or any other Unreal import setting.

engine-addressability remains Bevy-only.

Scale evidence and policy stay separated: #267 owns the measured scale domains and #268 owns the selected-node finding. The #155 advice contract reports an honest refusal for this profile. Static placement baking #224 does not replace the skinned rest/bind design #269.

Configure the profile

[engine]
profile = "unreal"
profile_revision = 1
engine_version = "5.8"
importer = "fbx-importer"

[clips."locomotion_*"]
loop = true
movement_owner_xz = "animation"
max_loop_position_delta_m = 0.01
max_loop_rotation_delta_deg = 1.0

Unreal revision 1 has no materialized setting vocabulary. Supplying [engine.settings] or clip engine settings is an error. generate import-advice returns the typed profile_settings_unmodeled refusal rather than inventing Convert Scene Unit, frame ranges, sample rates, or root-motion settings.

Revision 2 import advice

The exact revision-2 advice tuple is unreal / 2 / 5.8 / fbx-importer. It accepts FBX only and has one required document setting. There is no safe profile default, so the setting must be explicit:

[engine]
profile = "unreal"
profile_revision = 2
engine_version = "5.8"
importer = "fbx-importer"

[engine.settings]
sample_rate = "default_30"       # or "source_determined"
# sample_rate = "custom_hz(60)"   # 1..48000

The closed sample-rate domain is default_30, source_determined, or custom_hz(n) with n from 1 through 48,000. The unreal_fbx_import_data projection in urn:animsmith:schema:engine-import-advice:2 projection emits bUseDefaultSampleRate=true for default_30; the other two forms emit false and a CustomSampleRate value (0 for source_determined, otherwise the explicit hertz). No frame range, unit conversion, skeleton, compression, root-motion, or runtime result is inferred. The advice is a same-load projection of the declared setting, not an Unreal import execution or certification.

Common failures and fixes

SymptomEvidence to inspectCorrect owner
Animation is the wrong physical sizesource units, Unreal Convert Scene Unit, rest-world-scaleAuthor centimetre/metre intent in the DCC or choose the importer option explicitly.
Sequence truncates or imports the wrong rangesource take range, exact end-frame facet, Unreal Animation Length/Frame Import RangeFix the exact frame cut in the DCC or importer; the bounded Unreal check only tests the source end-frame lattice.
Animation-only import cannot bindrequired-bones, missing-bones, hierarchy and namesExport against the target Skeleton or retarget in Unreal/DCC.
Root motion does not drive the actorin-place, root-motion-speed, root role evidenceEnsure a root bone carries the intended motion and configure the Animation Sequence/graph.
Socket, IK, or collision attachment scales incorrectlyrest-world-scale source path and ancestryRepair inherited scale or use supported rest/bind reparameterization, then retest in Unreal.

Scale and unit workflow

Unreal's units guide uses centimetres for distance by default, while AnimSmith measurements are normalized to metres. Compare physical quantities, not the raw number printed in two different unit systems. The FBX import option Convert Scene Unit converts source units to centimetres. Inherited bone scale also multiplies descendant sockets, attachments, IK and collision anchors, animation translations, and root-motion distance.

Use scale whole-document only for self-contained glTF/GLB whose entire represented length domain is wrong. Use scale rest-bind when world geometry is correct but a selected skinned hierarchy carries a compensating uniform scale; narrow FBX rest/bind produces GLB. Use static transform baking only for unskinned placement. None of these operations chooses Unreal Skeleton, retarget, root-motion extraction, socket, IK, collision, or compression policy.

Before: a weapon socket inherits 100 while the mesh is visually compensated. After rest/bind: the proved world geometry and trajectories are unchanged and the selected hierarchy is canonicalized. Unreal import, socket attachment, physics asset, root motion, retargeting, and runtime playback still require engine validation. See Scaling safely.

Godot 4.7 animation profile

Use this page for godot / revision 1 / 4.7 / resource-importer-scene. The frozen profile accepts glTF JSON, GLB, and FBX. That is AnimSmith's evidence boundary, not a complete list of formats Godot can import.

What Godot expects

Godot's scene importer turns a source scene into Godot nodes and resources. Its advanced settings can slice animation timelines, optimize tracks, save animations externally, and configure subresources. Retargeting 3D skeletons depends on a BoneMap, compatible hierarchy, and aligned rest poses; matching bone names alone is not enough.

Godot documents a right-handed, Y-up, metre-oriented 3D convention. Its nodes/root_scale defaults to 1.0; nodes/apply_root_scale decides whether that value is applied into descendants, meshes, animations, and bones or left on the root node. AnimSmith revision 1 does not model either setting or predict the resulting imported hierarchy.

AnimSmith checks and thresholds

All ordinary checks remain engine-neutral. The most relevant are:

Check idExact default boundaryGodot-facing use
rest-world-scaleselected node factor 1.0 ± 0.0001 inclusiveFinds inherited scale before choosing root_scale/apply_root_scale.
scale-keyscomponent range greater than 1e-4Finds scale animation that can complicate retargeting.
non-uniform-scalerelative spread greater than 1e-4Flags a hierarchy/physics/attachment risk.
constant-nonunit-scaleunit deviation greater than 1e-4; off by defaultEnforce a project policy against retained scale channels.
bind-posemean first-frame/rest deviation greater than 45°Surfaces a likely rest-pose mismatch before BoneMap retargeting.
frozen-bonerequired bone rotation never exceeds 1°Finds wrongly mapped or static required roles.
loop-closure0.01 m and 1.0°Checks a declared source loop before Godot slicing/optimization.

engine-addressability is Bevy-only. Godot has no engine-specific prediction check in V1.

The measured parent/rest-world/bind domains come from #267, while #268 owns the selected-node finding. The #155 advice contract refuses rather than inventing Godot settings. Static placement baking #224 and the skinned rest/bind design #269 solve different transform problems.

Configure the profile

[engine]
profile = "godot"
profile_revision = 1
engine_version = "4.7"
importer = "resource-importer-scene"

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

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

Godot revision 1 has no setting descriptors. Any engine setting is rejected, and generate import-advice emits profile_settings_unmodeled. Configure timeline slices, root scale, animation optimization, external animation files, BoneMap, and root-motion behavior in Godot or the source DCC.

Revision 2 import advice

The exact revision-2 advice tuple is godot / 2 / 4.7 / resource-importer-scene. It accepts only glTF JSON and GLB for this document-level projection. The closed document setting vocabulary is:

[engine]
profile = "godot"
profile_revision = 2
engine_version = "4.7"
importer = "resource-importer-scene"

[engine.settings]
animation_fps = 30       # positive integer; 1..120
animation_trimming = false

animation_fps defaults to the verified profile value 30 when omitted and animation_trimming defaults to false; explicit values retain their explicit_config origin in the advice. The JSON prediction projects exactly animation/fps and animation/trimming under the godot_params projection kind in urn:animsmith:schema:engine-import-advice:2. It does not emit root scale, optimization, external-animation, BoneMap, timeline-slice, or runtime playback conclusions. Values are proposed importer parameters derived from same-load evidence, not a Godot import execution or certification.

Common failures and fixes

SymptomEvidence to inspectCorrect owner
Imported character has the wrong sizesource units, rest-world-scale, Godot root_scale and apply_root_scaleFix DCC units or set the importer deliberately; avoid stacked compensation.
Socket, IK target, or collision anchor is offsetselected-node ancestry and affine classRepair the hierarchy or canonicalize a supported rest/bind scale; verify after import.
Retarget pose twists or limbs driftrequired-bones, bind-pose, first-frame and hierarchy evidenceAlign rest pose/BoneMap in Godot or DCC.
One long source timeline is not usable as clipssource take inventory and intended cutsDefine slices in Advanced Import Settings or author separate clips in the DCC.
Loop pops after optimizationloop closure and velocity checksRepair the cut/source curves, then verify the imported optimized animation.

Scale and unit workflow

Godot's 3D introduction states that 3D uses metres and recommends authoring at the correct scale because post-import scaling can create precision problems. Keep three decisions separate:

  • Whole-file unit correction changes every represented length and is supported by scale whole-document only for self-contained glTF/GLB.
  • Skinned rest/bind reparameterization preserves world geometry while removing one declared compensating hierarchy scale; glTF/GLB and the narrow FBX-to-GLB path are supported.
  • nodes/root_scale is a Godot importer choice. AnimSmith does not emit it.

An inherited bone scale multiplies descendant sockets, attachments, IK and collision anchors, animation translations, and root-motion distance no matter which layer introduced the compensation.

Static transform baking fixes only supported unskinned placement. It cannot repair a Skeleton3D/BoneMap contract. After any source rewrite, retest attachments, IK, collision shapes, retarget pose, root motion, animation slices, and runtime playback in Godot. See Scaling safely.

Bevy 0.19.0 animation profile

Use this page for the exact bevy / 0.19.0 / gltf-asset-loader profiles. Revision 1 owns animation-label addressability. Revision 2 owns the unit/effective-scale prediction substrate and its version-pinned load settings. Revision 3 is the current slice for animation/channel gate support and the separately versioned rich addressability-rule bundle. All three accept glTF JSON and GLB only; none applies to a later Bevy release.

What Bevy expects

Bevy loads glTF subassets through typed labels. In 0.19.0, GltfAssetLabel::Animation(index) formats as Animation{index}. An animation name is separate optional metadata, not part of that typed selector. Runtime playback still requires the relevant animation feature/settings, loaded assets, an AnimationPlayer, targets, and an animation graph or equivalent application setup.

The frozen profile also records that animation target ids are derived from name paths. Revision 3's separately versioned rich addressability bundle emits bounded predictions for those paths, named-map winners, scene/skin labels, and the optional default-scene route. It does not emit graph templates, extension support, or proof that a runtime asset exists.

AnimSmith checks and thresholds

Check idExact boundaryBevy-facing use
engine-addressabilityno numeric tolerance; exactly one Animation{i} facet per completely inventoried source animationPredicts the canonical typed label spelling from source order. Partial inventory produces one unsuppressible required-unavailable inventory facet.
engine-unit-scaleexact revision-2 profile facts and settings; no tolerance and no content findingEmits exact 1:1 glTF-metre to Bevy world-length-unit mapping plus distinct loader-scene, primitive-child, and selected-source-node affine classifications. Missing required evidence is unsuppressible required-unavailable.
engine-track-supportexact revision-3 gate settings and bounded same-load animation/channel inventory; no content findingEmits only negative gate outcomes for source animation/channel rows, or required-unavailable evidence when inventory is incomplete or both gates allow loading. Extensions, other constructs, and positive runtime survival are outside this slice.
engine-addressability (rich V2)exact revision-3 Bevy addressability-rule bundle; no numeric tolerance or content findingEmits typed scene, default-scene, skin/IBM, named-map, and target path/UUID projections beside the unchanged Animation{i} facets. Incomplete, unreachable, multiply reachable, feature-disabled, or colliding work is required-unavailable.
rest-world-scaleselected node factor 1.0 ± 0.0001 inclusiveFinds authored inherited scale at attachment/IK nodes; it does not predict Bevy's imported result.
scale-keyscomponent range greater than 1e-4Finds animated scale before runtime transform propagation.
non-uniform-scalerelative spread greater than 1e-4Flags possible shear/attachment/physics consequences in composed transforms.
loop-closure0.01 m and 1.0°Checks declared loops before graph playback.
in-placeXZ speed at least 0.5 m/s counts as travellingMakes gameplay versus animation movement ownership explicit.

The rest of the check catalog is unchanged by the profile.

For scale, #267 owns the measurement domains and #268 owns the selected-node finding. The #155 import-advice work has no Bevy payload. Static placement baking #224 and skinned rest/bind reparameterization #269 remain separate from selector generation.

Configure and generate selectors

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

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

[checks.rest-world-scale]
expected_uniform_scale = 1.0
uniform_scale_tolerance = 0.0001
animsmith --config bevy.animsmith.toml lint --select engine-addressability character.glb
animsmith --config bevy.animsmith.toml generate addressability character.glb

The standalone addressability inventory retains source animation/channel indices, optional names, raw targets/accessors, and dependency closure. The optional Bevy section uses the same existing check. Named, unnamed, and duplicate-named animations remain distinct because source index is the authority. The selector can change when animation array order changes.

Bevy revision 1 has no settings vocabulary, so [engine.settings] and clip engine settings are invalid. generate import-advice is not a Bevy settings generator.

For unit/effective-scale prediction, select revision 2 and declare the two required environment facts. Defaults are retained with their origin; list them explicitly only when the load differs:

[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 # default
rotate_meshes = false       # default
load_meshes = "nonempty"    # default; or "empty"
load_animations = true      # default

[runtime_nodes]
selectors = ["weapon_socket", "ik_*_target"]
animsmith --config examples/bevy-v2.animsmith.toml lint \
  --select engine-unit-scale character.glb --format json

The handler environment is closed to an empty registry or exactly Bevy 0.19's stock PBR handler. Arbitrary application handlers are not approximated. With load_meshes = "empty", the rule reports each otherwise-created primitive child as suppressed while retaining the source-node entity semantics.

Revision 3: animation/channel gate support

Revision 3 preserves the revision-2 Bevy tuple and adds the narrow engine-track-support check. The only new settings are the document-scoped bevy_animation_feature and load_animations, each recorded with its explicit/default origin:

[engine]
profile = "bevy"
profile_revision = 3
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
load_animations = true

The compiled bevy_animation feature gate has precedence over load_animations. A disabled feature gate therefore predicts a negative drop even when load_animations = true; with the feature enabled, a false load_animations setting predicts the corresponding loader drop. These are negative importer outcomes, not content findings. If both gates allow loading, the result is a stable required-unavailable runtime-survival state because AnimSmith does not execute Bevy or inspect its runtime asset.

The rule inventories raw source animation rows and independent per-animation channel rows from the same load. Complete-empty source animation inventory is not applicable. Partial or unavailable animation/channel coverage emits exactly one subjectless, unsuppressible inventory required_prediction_unavailable facet and no retained-prefix prediction; bounded N+1 evidence is therefore distinguishable from a complete result. Extensions, unsupported constructs, target survival, graph wiring, and other positive runtime claims remain outside revision 3. Current output-v19 carries its immutable V5 provenance unchanged; the output-v17 and output-v16 readers and the revision-2/V4 output-v15 reader remain preserved.

Revision 3 rich addressability

The rich standalone contract is the immutable urn:animsmith:schema:gltf-addressability:2; see gltf-addressability-v2.schema.json. It is selected only for the exact revision-3 tuple and preserves the V1 gltf-animation-addressability:1 inventory as a nested animation domain. The adapter runs one engine-addressability evaluation and does not create a second lifecycle.

The separately versioned authority pins Bevy tag v0.19.0, commit c6f634ca9f406d68ba5109d921247b654cb42c10, bevy_gltf 0.19.0, locked gltf 1.4.1, and the 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 (independently-versioned helpers such as bevy_mikktspace retain their own version); its lock guard rejects internal Bevy patch drift. It requires explicit target pointer width (bits32 or bits64) because Bevy hashes path segment lengths using the target pointer width; AnimSmith never uses the host width. Missing bevy_animation or disabled load_animations is typed unavailable, not runtime success.

Scene{i} is emitted for every declared source scene. Gltf.default_scene is only a route to the selected existing Scene{i}: there is no DefaultScene label and no fabricated Scene0. Every source skin eagerly gets Skin{i}/InverseBindMatrices, including unreferenced skins, with Bevy's identity fallback when inverse-bind data is absent. Skin{i} is created when any source node references it during the all-source-node construction pass; source skin indices, never collected-vector position, are authoritative. Explicit skin.skeleton remains source evidence only because Bevy ignores it; scene-instantiated SkinnedMesh attachment is outside this static slice.

Named scene/animation/skin maps are separate from typed labels and are source-order last-write-wins; skin winners follow lazily created skins in first-reference order. Target rows are per unique source animation target node with contributing animation/channel identities. Authored names or GltfNode{source_index} fallbacks form the path, excluding the scene world-root. A target path/UUID is published only when complete hierarchy, reachability, closure, feature/settings, and collision evidence exists.

The report carries an independent target_coverage projection alongside the retained target rows. It is complete when the unique-target domain is exhaustively represented by complete raw node/scene/path and animation/channel evidence, including an empty domain; incomplete evidence makes it required_unavailable. It also becomes required_unavailable with target_domain_truncated when more than 4,096 targets exist, or with projection_bounds_exceeded when a new rich projection exceeds its aggregate structural/text budget.

Each new rich projection scene/node/skin/attachment/path/target/map domain is capped at 4,096 rows, with aggregate projection structural references at 65,536 and dynamic projection text at 1 MiB. The sealed V1 animation inventory and CheckEvaluation scopes retain their own bounds. One name/path segment is capped at 1,024 UTF-8 bytes, a path at 4,096 bytes and 256 segments, and a report at 256 MiB. The explicit target-coverage projection reports target_domain_truncated at the target row ceiling and projection_bounds_exceeded for other rich-projection budget exhaustion; readback rejects N+1 collections and contradictory states. This is prediction evidence only and does not certify Bevy loading, spawning, target survival, graph wiring, or playback.

If the shared check's core per-file facet budget would be exceeded, V2 exposes one subjectless engine-addressability:facet-budget scope with facet_budget_exceeded rather than a misleading retained-facet prefix. This is compaction within the same check lifecycle, not an additional check.

Common failures and fixes

SymptomEvidence to inspectCorrect owner
Code loads the wrong animation after a source editgenerated source index and Animation{i} selectorRegenerate the manifest and update the code/asset contract; a numeric typed label is not stable across reordering.
Selector exists but nothing playsdependency closure, Bevy loader settings/features, AnimationPlayer, graph and targetsFix runtime loading/graph wiring; the selector prediction does not prove existence.
Duplicate names collide in application lookupsource names and distinct indicesUse typed index labels or define an application-owned name policy.
Attachment or IK target is scaled/shearedrest-world-scale path, ancestry, affine classRepair source hierarchy or supported rest/bind scale; validate the composed Bevy transform at runtime.
Clip loops or movement ownership disagree with gameplayloop checks, in-place, root-motion-speedFix source/contract and graph/controller behavior.

Scale and unit workflow

glTF defines all linear distances in metres. Revision 2 proves that one glTF metre-valued length reaches one Bevy world-space length unit with no loader scale conversion; it deliberately does not claim a universal application, physics, or gameplay metre convention. Treat the format mapping and the application's world convention as two separate authorities. Inherited scale still multiplies descendant attachment, IK, and collision offsets, authored animation translations, and root travel in the composed transform hierarchy.

Use scale whole-document only when every represented glTF length is wrong. Use scale rest-bind when geometry is already correct and one skinned hierarchy carries a declared compensating uniform scale. Use static transform baking only for unskinned placement. None of these operations proves Bevy scene spawning, target-id paths, attachment behavior, physics/collision scale, root motion, animation-graph wiring, or visual playback.

Before: a selected weapon node has a world affine classification that includes uniform factor 0.01. After rest/bind: AnimSmith proves preserved geometry and joint trajectories under the reparameterized hierarchy. Then load the emitted asset in the exact Bevy application, validate the caller-owned WorldAssetRoot and ancestors that are outside importer output, resolve the typed label, attach the prop, and exercise the graph and physics setup. See Scaling safely.

glTF and generic runtime profile

Use this page when glTF/GLB is consumed by a custom runtime or when no frozen engine tuple matches the actual consumer. This is intentionally not a sixth V1 engine profile. Leave [engine] absent and use AnimSmith's engine-neutral measurements, checks, transforms, and evidence.

What the format guarantees

The glTF 2.0 specification defines a right-handed, Y-up coordinate system and metres for linear distances. Animation sampler input is time in seconds and must be finite, non-negative, and strictly increasing; channels target node translation, rotation, scale, or morph weights. Names are optional and not guaranteed unique. The runtime still chooses scene selection, asset addressability, graph/controller behavior, root-motion extraction, blending, events, masks, physics, and error handling.

AnimSmith checks and thresholds

No engine-specific check applies without a profile. The engine-neutral catalog still provides exact source contracts:

Check idExact default boundaryGeneric-runtime use
time-monotonicstrict increase; negative tolerance 0.0001 s; late-first-key note after 0.017 sEnforces the glTF timing requirement and identifies an unauthored clamp-held prefix.
quat-normabsolute length deviation greater than 0.001Prevents unstable runtime interpolation.
scale-keyscomponent range greater than 1e-4Makes animated scale explicit.
non-uniform-scalerelative spread greater than 1e-4Flags a transform/skin/physics portability risk.
rest-world-scaleselected node factor 1.0 ± 0.0001 inclusiveChecks project-declared attachment/IK nodes without guessing units from bounds.
loop-closure0.01 m and 1.0°Checks a declared cyclic endpoint.
loop-seam-vel / loop-seam-rot0.1 m/s / 5.0°/sFinds wrap velocity discontinuity.
in-placeXZ speed at least 0.5 m/s counts as travellingEnforces declared movement ownership.

Use the complete check catalog for structural rig, duration, FPS, gait, sync, foot-slide, and bind-pose contracts. Do not select engine-addressability; it is defined only for the exact Bevy tuple.

The scale-domain measurements are the engine-neutral output of #267, and the selected-node policy is #268. The profile-specific advice work in #155 does not invent a generic importer. Static placement baking #224 and skinned rest/bind reparameterization #269 remain distinct repair contracts.

Configure the runtime contract

Declare only what the project actually knows:

[rig]
profile = "humanoid"

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

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

[clips."locomotion_*"]
loop = true
movement_owner_xz = "gameplay"
speed_mps = { value = 3.0, tolerance = 0.2 }

There is no fallback or nearest engine profile. Adding a made-up [engine] tuple is an operator error, not a way to label a custom runtime.

Common failures and fixes

SymptomEvidence to inspectCorrect owner
Asset is the wrong physical sizeglTF metre contract, source exporter, rest-world-scaleCorrect all lengths in DCC/whole-document conversion, or repair one compensating hierarchy scale.
Animation name lookup is ambiguoussource animation indices and optional namesDefine a runtime-owned unique id policy; glTF names are not identities.
Attachment, IK, or collision anchor driftssource node path, ancestry, affine classRepair transforms or canonicalize supported rest/bind scale, then test runtime composition.
Loop or movement slidesloop checks, in-place, root-motion-speed, foot-slideAlign source motion, declared intent, and controller behavior.
Valid glTF still fails to loaddependency closure, required extensions, runtime logsFix resource packaging/extension support in the exporter or runtime.

Scale and unit workflow

If an exporter wrote centimetre-valued numbers into a format that defines metres, the whole asset is physically wrong: a factor 0.01 whole-document conversion is the relevant operation when the supported-source boundary is met. If the mesh is already the correct world size but a skeleton root carries 0.01 with compensating descendants, whole-document conversion is wrong; use the explicit rest/bind operation instead. That inherited scale also multiplies descendant socket, attachment, IK, and collision-anchor offsets, animation translations, and root-motion distance.

Static transform baking moves supported unskinned placement into geometry. It does not repair a skinned hierarchy. Rest/bind reparameterization covers the accepted skinned design and preserves its stated world-space invariants. The runtime still owns sockets, attachments, IK, collision anchors, root motion, graph behavior, extensions, rendering, physics, and visual acceptance. See Scaling safely and the static asset workflow.

animsmith examples

A cookbook of runnable workflows. Each section is a self-contained task you can copy into your own project — several double as CI/acceptance gates. Each workflow addresses a runtime failure described in what game-ready means, which explains why the checks behind these commands exist. For the larger process that turns raw source assets into engine-facing outputs, see the pipeline scenario guide.

Commands that reference examples/assets/ run against small assets committed there, so you can follow along from a source checkout with no downloads; the assets are procedurally generated (see their README for provenance and how to regenerate them). The conversion and reporting section operates on assets you supply, using placeholder filenames — export.fbx, old.glb — for your own exports and baselines.

For complete contract semantics, see the configuration reference.

Console transcripts are real command output, with long finding messages elided as .... JSON envelopes are abridged illustrative projections with placeholder digests, byte counts, and build provenance; capture those identity fields from your own run. Exit-code annotations remain # comments.

Running the commands

Examples use the installed CLI form, animsmith <command>. From a source checkout, prefix each command with cargo run -p animsmith --:

animsmith lint examples/assets/clip.glb
cargo run -p animsmith -- lint examples/assets/clip.glb   # source checkout

Two examples need the default build's feature-gated commands (report, convert); they are marked default features only. Everything else works in the pure-Rust --no-default-features build too.

Exit codes

Every example relies on the same convention, so scripts can gate on it:

CodeMeaning
0No failing findings and no required-unavailable engine prediction facets; warnings, notes, and ordinary coverage gaps may remain.
1A failing finding, any required_prediction_unavailable facet, a significant diff, or pending fix --dry-run repairs.
2Operator error: unreadable input, bad config, bad flags.

1. A first CLI gate

When a clip enters CI, the first question is not just whether the file parses — it is whether the motion is safe to ship. A gate built from inspect, measure, and lint catches the gap described in a valid file is not a usable clip: valid containers can still hide content problems that only show up after import, bake, or playtest. inspect summarizes structure; measure reports metrics without judgment; lint runs the checks and sets the exit code.

$ animsmith inspect examples/assets/clip.glb
examples/assets/clip.glb
rig profile: none detected
skeleton: 3 bones
  root
    spine
      chest
materials: 0
mesh instances: 0
clips: 1
  swing: 1.000s, 1 tracks, 5 keys max

$ animsmith lint examples/assets/clip.glb
examples/assets/clip.glb:
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; ...
0 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), ...   # exits 0

A defective asset produces findings and a non-zero exit:

$ animsmith lint examples/assets/clip-dirty.glb
examples/assets/clip-dirty.glb:
  error[quat-norm] clip 'swing' bone 'spine' @0.500s: non-unit rotation key ...
  warning[quat-flip] clip 'swing' bone 'spine' @0.750s: 2 hemisphere flip(s) ...
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; ...
1 error(s), 1 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), ...   # exits 1

Warnings alone keep the exit code at 0. Use --deny-warnings when CI should fail on warnings too:

$ animsmith lint --deny-warnings examples/assets/clip-dirty.glb   # exits 1

For machine consumption, --format json emits the current v19 result envelope (see output.md). This jq projection keeps the example short while showing where retained/promotion evidence, content findings, and independently versioned measurement evidence live:

$ animsmith lint --format json examples/assets/clip-dirty.glb | jq \
    '{schema_version, schema, command,
      prediction_facets: .summary.prediction_facets,
      prediction_provenance: .files[0].prediction_provenance,
      input: .files[0].input,
      check: (.files[0].checks[] | select(.check_id == "quat-norm") |
        {check_id, selection, configuration, applicability, evaluation, findings}),
      measurements: (.files[0].measurements | {schema_version, schema})}'
{
  "schema_version": 19,
  "schema": "urn:animsmith:schema:output:19",
  "command": "lint",
  "prediction_facets": {
    "available": 0,
    "required_prediction_unavailable": 0
  },
  "prediction_provenance": null,
  "input": {
    "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "bytes": 123456
  },
  "check": {
    "check_id": "quat-norm",
    "selection": "selected",
    "configuration": "enabled",
    "applicability": "applicable",
    "evaluation": "complete",
    "findings": [
      { "check_id": "quat-norm", "severity": "error", "clip": "swing",
        "bone": "spine", "time_s": 0.5, "measured": 1.05, "expected": 1.0,
        "message": "non-unit rotation key (worst at key 2)" }
    ]
  },
  "measurements": {
    "schema_version": 18,
    "schema": "urn:animsmith:schema:measurements:18"
  }
}

As a CI gate

Lint every clip and fail the job on any error (add --deny-warnings to also fail on warnings):

# .github/workflows/animate.yml
- run: cargo install animsmith --no-default-features
- run: animsmith lint --deny-warnings clips/*.glb

The step's exit code is the gate: 1 fails the job, 0 passes it.


2. Repairing an asset

Use repair when a clip plays as a sudden flicker, spin, or explosion, but the authored motion is still recoverable. The symptoms in the pose flickers, spins, or explodes are often quaternion representation problems, so fix --dry-run lets a gate report the exact lossless repairs before writing anything.

quat-norm and quat-flip are not just checks — they are lossless, idempotent repairs. fix --dry-run is the check mode: it reports what it would repair and exits 1 if anything is pending, writing nothing.

$ animsmith fix examples/assets/clip-dirty.glb --dry-run
  would fix[quat-norm] clip 'swing' bone 'spine': 1 key(s) unit-normalized
1 key(s) would be fixed across 1 track(s) -> no output written
  would fix[quat-flip] clip 'swing' bone 'spine': 1 key(s) hemisphere-normalized
1 key(s) would be fixed across 1 track(s) -> no output written   # exits 1

Write the repaired asset with -o (or --in-place), then confirm it lints clean:

$ animsmith fix examples/assets/clip-dirty.glb -o fixed.glb
  fixed[quat-norm] clip 'swing' bone 'spine': 1 key(s) unit-normalized
1 key(s) fixed across 1 track(s) -> fixed.glb
  fixed[quat-flip] clip 'swing' bone 'spine': 1 key(s) hemisphere-normalized
1 key(s) fixed across 1 track(s) -> fixed.glb

$ animsmith lint fixed.glb
fixed.glb:
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; ...
0 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), ...   # exits 0

Because the repairs are lossless, diff confirms no measurement moved — the fix changed representation, not motion:

$ animsmith diff examples/assets/clip-dirty.glb fixed.glb
no significant movement
0 significant change(s)                      # exits 0

Pin an exact repair set with --repair id[,id] (animsmith fix --help lists the ids). Repairs that cannot be applied byte-surgically — data-URI .gltf buffers, cubic tracks, quantized rotations — are reported as skipped[...] and do not fail the check. Gate on lint when detection alone should fail CI.


3. Editing a clip

Use editing when the animation content is right but the pipeline cut is wrong: a capture has junk at the head, a one-shot needs a final hold, or a loop's stride anchor lands in the wrong place. Those are the cases in the clip is the wrong length or freezes at the end and the loop pops, where a mechanical transform can make the clip conform without touching geometry.

transform applies mechanical pipeline edits — slice a window, hold the final pose, re-anchor a gait cycle, remove an eligible duplicate loop endpoint, or prune provably constant multi-key tracks. Geometry passes through unchanged.

Slice a sub-window (retimed to start at 0):

$ animsmith transform examples/assets/clip.glb -o sliced.glb --slice 0.5:1.0
  sliced 'swing' to [0.5:1]s (3 keys max)
wrote sliced.glb (3 node(s), 1 clip(s), 0 mesh(es) / 0 position(s), 0 material(s))

$ animsmith diff examples/assets/clip.glb sliced.glb
  swing duration_s: moved 1.0000 -> 0.5000
  swing frame_count: moved 5.0000 -> 3.0000
  swing loop_continuity.bones[1].rotation_delta_deg: moved 22.9183 -> 11.4592
  swing loop_continuity.bones[2].position_delta_m: moved 0.1987 -> 0.0998
  swing loop_continuity.bones[2].rotation_delta_deg: moved 22.9183 -> 11.4592
  swing loop_continuity.bones[2].seam_velocity_delta_mps: moved 0.0598 -> 0.0200
  swing bone_rotation_range_deg[spine]: moved 22.9183 -> 11.4591
7 significant change(s)                       # exits 1

Extend the final pose (useful for hold frames at the end of a one-shot):

$ animsmith transform examples/assets/clip.glb -o held.glb --hold-extend 0.5
  hold-extended 'swing' by 0.5s
wrote held.glb (3 node(s), 1 clip(s), 0 mesh(es) / 0 position(s), 0 material(s))

Other transforms: --gait-anchor rotates a cyclic clip so its stride anchor lands at t=0. It is an explicit in-place declaration and needs resolvable Root (or Hips fallback), Hips, and feet roles; accumulating root translation/yaw refuses without writing the output or emitting buffered success stdout. Every nonconstant selected trajectory channel must contain exactly one key at each --fps whole-frame sample over the clip duration; sparse, differently framed, duplicate-time, or off-grid evidence and duplicate (bone, property) channels refuse. Declared frames × skeleton bones, declared frames × tracks, and maximum authored keys × skeleton bones are each capped at 1,000,000 samples. Retain root motion or use runtime phase offsets instead. --fps N sets the grid used for retiming. See cli.md for the full flag list.

For a loop exported with frame 0 copied again at the inclusive final frame, use --drop-duplicate-loop-endpoint only after the default warning identifies the strict authored-key case:

$ animsmith transform cycle.glb -o cycle-open.glb --drop-duplicate-loop-endpoint

The transform removes the same closing key count from every channel and re-pins the duration; it does not repair a stationary hold, root-travel/nonclosing clip, mismatched timelines, endpoint tangents, or retargeting damage. The open cycle is for engines that wrap over duration, so its inclusive loop-closure result is expected to change.

For exported clips with redundant “key everything” or baked-control channels, start with the default constant-track note, inspect the DCC/engine result, then make the explicit mechanical edit:

$ animsmith lint exported.glb --select constant-track
$ animsmith transform exported.glb -o compact.glb --prune-constant-tracks
  constant-track removed 'idle': track index 3 bone 'control' scale Linear 90 key(s)

The transform keeps a deterministic text record for every removal and every candidate it declines, including the source track index. It leaves an animates_bones target in place so the declared motion contract can still be checked; it does not protect [rig] required_bones, which only declares that the skeleton must contain a name. Re-lint and preview compact.glb in the target engine before replacing the export.


4. A project contract config

Use a contract config when the failure depends on what your game expects, not on file validity alone. The character glides or runs in place, feet skate when clips blend, and feet slide within one clip all need declared locomotion or blend assumptions before animsmith can judge them.

Mechanical checks run with no config. Contract-aware checks run only for the expectations you declare. duplicate-loop-endpoint, loop-closure, loop-seam-vel, and loop-seam-rot need only loop = true; role-dependent checks such as loop-seam, gait-group, root-motion-speed, in-place, and foot-slide also need resolvable rig roles. Without those roles they report a typed coverage gap rather than guess, so a config that pins a [rig] profile (or inline [rig.roles]) is what makes them fire.

sync-group is role-independent: use it for same-time or absolute-sync blend sets, where compatible duration, longest-channel key count, validated declared FPS grid, and loop endpoint mode matter. Each group declares non-negative duration, key-count, and FPS spread tolerances; incompatible findings retain a machine-readable row for every configured member.

The optional nested time_complement policy activates the separate warning-level time-complement check for every unordered pair in that sync group. It uses the already measured left-minus-right foot-height gait phase, so it needs resolvable hips plus left and right foot/toe roles. The exact-zero signal has no phase subject even when min_lr_amplitude_m = 0; positive near-idle or noisy phase evidence below that floor is also excluded. Among the remaining pairs, a warning appears only when reflected-time similarity beats same-time similarity by more than min_reflected_time_advantage. The finding's member rows retain both scores, their advantage, each phase, and each amplitude.

examples/assets/walk.glb is a committed rig for this: a hips + two-foot skeleton with a one-second walk cycle. Its bone names resolve a built-in profile, so inspect binds the rig with no config at all:

$ animsmith inspect examples/assets/walk.glb
examples/assets/walk.glb
rig profile: ue-mannequin (3 roles)
  hips         -> pelvis (exact)
  left_foot    -> foot_l (exact)
  right_foot   -> foot_r (exact)
skeleton: 3 bones
  pelvis
    foot_l
    foot_r
materials: 0
mesh instances: 0
clips: 1
  walk: 1.000s, 2 tracks, 33 keys max

measure reports the semantic metrics the checks judge — per-bone C0 pose closure and C1 seam velocity, the feet-relative loop-seam ratio (≈ 0 here, since this cycle returns its feet exactly), gait phase, L/R foot amplitude, and sampled Hips-fallback trajectory. This fixture's pelvis stays fixed, so all trajectory values are zero while the selected source remains explicit:

$ animsmith measure --format json examples/assets/walk.glb
{
  "schema_version": 19,
  "schema": "urn:animsmith:schema:output:19",
  "tool": { "name": "animsmith", "version": "0.14.0",
            "source": { "revision": null, "dirty": null } },
  "command": "measure",
  "summary": { "files": 1 },
  "files": [
    {
      "path": "examples/assets/walk.glb",
      "input": {
        "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
        "bytes": 123456
      },
      "rig": { "profile": "ue-mannequin", "resolution_outcome": "coverage",
        "resolved_roles": { "hips": "pelvis", "left_foot": "foot_l", "right_foot": "foot_r" },
        "resolved_role_policies": { "hips": "exact", "left_foot": "exact", "right_foot": "exact" } },
      "measurements": {
        "schema_version": 18,
        "schema": "urn:animsmith:schema:measurements:18",
        "clips": { "walk": {
          "duration_s": 1.0, "frame_count": 33,
          "animated_bones": ["foot_l", "foot_r"],
          "bone_channels": [
            { "bone_index": 1, "bone_name": "foot_l",
              "properties": ["translation"] },
            { "bone_index": 2, "bone_name": "foot_r",
              "properties": ["translation"] }
          ],
          "bone_rotation_range_deg": {},
          "loop_continuity": { "bones": [
            { "bone_index": 0, "bone_name": "pelvis",
              "availability": "measured",
              "position_delta_m": 0.0, "rotation_delta_deg": 0.0,
              "seam_velocity_delta_mps": 0.0,
              "seam_angular_velocity_delta_degps": 0.0 },
            { "bone_index": 1, "bone_name": "foot_l",
              "availability": "measured",
              "position_delta_m": 3.7e-17, "rotation_delta_deg": 0.0,
              "seam_velocity_delta_mps": 0.0,
              "seam_angular_velocity_delta_degps": 0.0 },
            { "bone_index": 2, "bone_name": "foot_r",
              "availability": "measured",
              "position_delta_m": 3.7e-17, "rotation_delta_deg": 0.0,
              "seam_velocity_delta_mps": 0.0,
              "seam_angular_velocity_delta_degps": 0.0 }
          ] },
          "loop_continuity_availability": "measured",
          "loop_endpoint_mode_availability": "not_applicable",
          "frame_grid_availability": "not_applicable",
          "loop_seam_ratio": 1.2e-15,
          "loop_seam_ratio_availability": "measured",
          "gait": { "phase": 0.75, "phase_availability": "measured", "lr_amplitude_m": 0.2 },
          "gait_availability": "measured",
          "root_trajectory": {
            "bone_index": 0, "bone_name": "pelvis",
            "source_role": "hips_fallback",
            "translation": {
              "horizontal_displacement_x_m": 0.0,
              "horizontal_displacement_z_m": 0.0,
              "horizontal_travel_m": 0.0,
              "vertical_displacement_m": 0.0,
              "vertical_min_displacement_m": 0.0,
              "vertical_max_displacement_m": 0.0
            },
            "translation_availability": "measured",
            "yaw": {
              "heading_axis": "positive_z", "net_yaw_deg": 0.0,
              "unwrapped_yaw_deg": 0.0, "yaw_travel_deg": 0.0
            },
            "yaw_availability": "measured"
          },
          "root_trajectory_availability": "measured",
          "speed_mps": 0.0,
          "speed_mps_availability": "measured"
        } },
        "mesh_definitions": [],
        "node_instances": [],
        "scenes": [],
        "skeleton_source_coverage": "unavailable",
        "skeleton_nodes": [],
        "skins": [],
        "material_resource_coverage": "complete",
        "material_definitions": [],
        "textures": [],
        "images": []
      }
    }
  ]
}

examples/walk.animsmith.toml is the contract: it declares the clip a loop (which arms the loop checks) and in-place, and caps their tolerances. Against the clean rig every semantic check passes, and the run exits 0. The one warning is not a defect in the motion: a cycle whose last key repeats its first is a representation an engine can trim, which editing a clip covers with transform --drop-duplicate-loop-endpoint.

$ animsmith lint --config examples/walk.animsmith.toml examples/assets/walk.glb
examples/assets/walk.glb:
  warning[duplicate-loop-endpoint] clip 'walk' @1.000s: declared loop repeats its first pose ...
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; ...
0 error(s), 1 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), ...   # exits 0

examples/assets/walk-dirty.glb is the same rig with the clip cut a quarter-cycle short, so the feet never return to their first-frame pose — the classic popped loop seam. The same contract catches it:

$ animsmith lint --config examples/walk.animsmith.toml examples/assets/walk-dirty.glb
examples/assets/walk-dirty.glb:
  error[loop-closure] clip 'walk' bone 'foot_r' @1.000s: loop does not close in position: ...
  error[loop-seam] clip 'walk' @1.000s: loop seam pops: wrap discontinuity is 6.82× ...
  error[loop-seam-vel] clip 'walk' bone 'foot_r' @1.000s: loop velocity changes at the seam: ...
  coverage[bind-pose] insufficient_rotation_evidence ×1 (scopes: first_frame_rest_delta; ...
3 error(s), 0 warning(s), 0 note(s), 1 coverage gap(s), 0 available prediction facet(s), ...   # exits 1

The contract is load-bearing: a bare animsmith lint examples/assets/walk-dirty.glb (no config) reports no findings — with no loop = true declared, the three loop checks are explicitly not applicable. Semantic checks enforce your declared expectations, not a guess.

walk-dirty.glb is one of a set. examples/assets/ commits one mutation of this same walk per runtime symptom — a channel that ends early, a clip that travels, a blend ring whose members disagree, a frozen arm, an animated non-uniform scale — each alongside the contract config that arms its checks. The asset table lists what every fixture fires and which config to pass.

Keeping the exported rig shape stable

animates_bones is deliberately per clip: it says that a named bone must exist and carry animation keys in that take. That is the right contract for a swinging arm or a locomotion root, but it is too strict for a static weapon socket, an IK target, a retargeting mask bone, or another runtime attachment point. Put those structural requirements on the rig instead:

[rig]
profile = "auto"
required_bones = ["root", "weapon_socket", "ik_hand_l"]

required-bones checks only that each declared name identifies exactly one bone in the input skeleton. A present-but-static bone is clean; a missing name is an error. Duplicate skeleton names are ambiguous rather than silently matched, and an export with no skeleton cannot satisfy the contract. This is also useful for a static mesh or a take with no clips: the structural check does not need animation data to run.

It is not a repair tool. AnimSmith does not add sockets, rename a duplicate, retarget a rig, or decide which bones your engine needs; fix the source rig or its export in the DCC, then lint it again. Use [clips.<name>] animates_bones = [...] when the same bones must move in a particular clip, and frozen-bone when keyed rotation is required to exceed a meaningful motion floor.

Presence and transform policy are separate. To require a runtime-facing node to inherit a unit effective rest scale, select it explicitly:

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

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

An exact name or * glob must match exactly one source node. A miss or multiple matches produces explicit coverage instead of guessing. The finding's node path includes source indices and ancestors, so a local scale of 1 can still be traced to a non-unit effective scale inherited from a parent. This does not inspect animation scale keys, infer units from geometry, or rescale the file. The legacy [checks.rest-world-scale].node_selectors spelling is an alias; declaring it together with [runtime_nodes] is a configuration error.

Scaling up to a full character

examples/character.animsmith.toml is the full game-character shape the small walk contract grows into:

[rig]
profile = "auto"            # or mixamo / ue-mannequin / humanoid

[checks.loop-seam]
max_ratio = 1.6             # per-check tuning
[checks.loop-closure]
max_position_delta_m = 0.01
max_rotation_delta_deg = 1.0
[checks.loop-seam-vel]
max_velocity_delta_mps = 0.1
[checks.loop-seam-rot]
max_angular_velocity_delta_degps = 5.0
[runtime_nodes]
selectors = ["weapon_socket", "ik_*_target"]
[checks.rest-world-scale]
expected_uniform_scale = 1.0
uniform_scale_tolerance = 0.0001
[checks.frozen-bone]
min_rotation_deg = 0.5
[checks.quat-flip]
severity = "note"           # demote while an upstream fix lands

[clips."run_*"]             # glob: every run clip loops
loop = true
movement_owner_xz = "animation" # extracted root motion owns horizontal travel
movement_owner_y = "gameplay"   # controller owns vertical motion
movement_owner_yaw = "animation"
max_loop_position_delta_m = 0.04  # per-family pose/velocity seam caps
max_loop_rotation_delta_deg = 2.0
max_loop_velocity_delta_mps = 0.2
max_loop_angular_velocity_delta_degps = 200.0
[clips.run_forward]
max_loop_rotation_delta_deg = 0.5 # exact entry wins for this dimension
duration_s = { value = 1.033, tolerance = 0.02 } # authored clip-length contract
speed_mps = { value = 3.1, tolerance = 0.25 }   # root-motion contract

[gait_groups.run-ring]      # a directional blend ring
clips = ["run_forward", "run_backward", "run_left", "run_right"]
max_gait_phase_spread = 0.15   # members must stride in phase
min_lr_amplitude_m = 0.03      # exclude near-idle members

[sync_groups.run-ring]
clips = ["run_forward", "run_backward", "run_left", "run_right"]
max_duration_delta_s = 0.001
max_frame_count_delta = 0
max_fps_delta = 0.01

[sync_groups.run-ring.time_complement]
min_reflected_time_advantage = 0.25 # reflected score must beat same-time score
min_lr_amplitude_m = 0.03           # skip low-confidence gait signals

A gait_groups block is the payoff for a real character: it holds every clip in a directional blend ring to the same stride phase, so runtime blends between them don't skate. animsmith.toml is auto-loaded from the working directory, so committing one next to your assets makes every bare animsmith lint enforce the contract.

Movement ownership is independent on XZ, Y, and yaw. Each field is either "gameplay" (the entity/controller owns that component) or "animation" (extracted root motion owns it); omitted axes stay absent. in_place = true and false remain compatibility aliases for XZ gameplay and animation, respectively, but one selector entry cannot use both in_place and movement_owner_xz. Exact/glob layers normalize the alias before applying the usual field-by-field precedence.

The four role-free loop-continuity caps (max_loop_position_delta_m, max_loop_rotation_delta_deg, max_loop_velocity_delta_mps, and max_loop_angular_velocity_delta_degps) refine the corresponding global check caps in a [clips.<exact-name>] entry or [clips."glob_*"] family. An exact name wins over matching globs and an omitted field keeps its inherited cap. This is useful when root-motion runs need a different position allowance but idles must stay tightly closed. All four values must be finite and non-negative.

Predicting a Bevy animation selector

The small bevy.animsmith.toml profile selects Bevy 0.19.0's exact glTF loader contract. Its first production prediction maps each completely inventoried source animation index to the canonical typed subasset label spelling Animation{index}:

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

The available facet's subject is the predicted display label, such as Animation0. Names do not control this selector: unnamed animations work, and duplicate names remain distinct indices. This predicts the selector spelling, not that Bevy loaded the file, retained its targets, or wired it into an animation graph. Incomplete source-animation inventory produces a blocking required_prediction_unavailable facet instead of guessing from the retained prefix. Prediction provenance v1 separately caps actual clip settings at 4,096, so a 4,097-clip file is a bounded operator error rather than a truncated prediction; issue #485 owns a future overflow representation.

Revision 2 uses bevy-v2.animsmith.toml for the unit/effective-scale rule. The example selects the closed empty-handler environment, declares the compiled animation feature, and demonstrates shared runtime-node selectors:

$ animsmith --config examples/bevy-v2.animsmith.toml lint \
    --select engine-unit-scale examples/assets/walk.glb --format json

The available file result is an exact 1:1 glTF-metre to Bevy world-length-unit mapping, not a claim that arbitrary application world state uses metres. Scene, primitive-child, and selected-node facets remain distinct; incomplete evidence is blocking rather than inferred.

Revision 3 is the current example in bevy-v3.animsmith.toml. It selects the narrow engine-track-support gate-support prediction:

$ animsmith --config examples/bevy-v3.animsmith.toml lint \
    --select engine-track-support examples/assets/walk.glb --format json

The prediction binds raw animation rows and independent per-animation channel coverage from the same load. The compiled bevy_animation feature gate takes precedence over load_animations; a disabled gate yields only negative dropped animation/channel outcomes, never a content finding. If both gates allow loading, runtime survival is required-unavailable because this contract does not run Bevy. Complete-empty inventory is not applicable; partial or unavailable inventory yields one subjectless, unsuppressible inventory facet and no retained-prefix prediction, including bounded N+1 overflow. Extensions, other animation constructs, and positive runtime survival are outside this slice. V5 provenance remains immutable inside the current output-v19 envelope; output-v17, output-v16, the rev1/rev2 profiles, and output-v15 contracts remain preserved.

Generate a reusable engine-neutral inventory as canonical JSON without a profile:

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

Select the exact profile to add the unchanged Bevy evaluation to the same document; the neutral inventory and its identity stay unchanged:

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

The inventory keeps source animation and channel indices, optional source names, raw targets and accessors, and dependency-closure identity. It does not infer scenes, skins, named-map winners, target UUIDs, extension support, or successful runtime loading.

The richer exact-Bevy path is a separate V2 contract, urn:animsmith:schema:gltf-addressability:2; see gltf-addressability-v2.schema.json. It retains bounded same-load scene, node, skin, attachment, root-to-node path, default-scene, named-map, and unique-target evidence while preserving this V1 animation inventory. Complete empty coverage proves absence; partial prefixes and unavailable domains do not. Explicit skin.skeleton is retained as source evidence, not inferred into a Bevy root, and scene-instantiated SkinnedMesh attachment is not claimed.

The V2 adapter is pinned to Bevy 0.19.0 revision 3 and commit c6f634ca9f406d68ba5109d921247b654cb42c10 (bevy_gltf 0.19.0, locked gltf 1.4.1, and root Cargo.lock). It reuses one engine-addressability lifecycle and the existing Animation{i} primitive. Scene{i} is emitted for every declared scene, and Gltf.default_scene routes only to an existing scene; there is no DefaultScene label or fabricated Scene0. Bevy eagerly emits Skin{i}/InverseBindMatrices for every source skin, including unreferenced skins, and emits Skin{i} when any source node references it. Named maps are separate source-order last-write-wins projections.

Exact target UUIDs require explicitly selecting 32- or 64-bit target pointer width because Bevy hashes segment lengths using the target pointer width; AnimSmith never infers width from the host. Missing feature/settings, incomplete or unreachable targets, multiple paths, and path/UUID collisions are typed required_unavailable, not guessed. target_coverage distinguishes complete (including empty) target coverage from incomplete raw/animation evidence and target_domain_truncated; other rich projection budget failures use projection_bounds_exceeded. Each V2 domain is capped at 4,096 rows, references at 65,536, path segments at 1,024 bytes and 256 segments, retained text at 1 MiB, and reports at 256 MiB. This remains prediction evidence only, not runtime-load, spawning, target-survival, graph, or playback evidence.

Predicting Unity Generic root motion

Select an exact engine profile and fully materialize every required importer setting before generating advice. For Unity Generic, for example:

[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."*"]
loop = true
movement_owner_xz = "animation"

[clips."*".engine_settings]
root_rotation = "extract"
root_position_y = "bake"
root_position_xz = "extract"
$ animsmith --config unity.animsmith.toml lint \
    --select engine-root-motion export.fbx

The current Generic revision-2 lint prediction compares the three declared movement-owner axes with the three bake | extract clip settings. bake means baked_into_pose; extract means stored_as_root_motion. It requires a case-sensitive, byte-exact Reference/Root path to the explicitly resolved Root role and never uses Hips as a fallback. The path grammar has no escaping or Unicode normalization, rejects empty/reserved/control/format segments, and limits segments, paths, and depth to 1,024 bytes, 4,096 bytes, and 256 segments. Raw path evidence is projected from the same FBX bytes; the implicit root and generated helpers cannot match.

An available facet records a typed routing result; a conflict is an error finding. Missing path, Root, intent, settings, raw inventory, or measured translation/yaw availability is required_prediction_unavailable, not a content finding, and exits 1. Measurement magnitude does not affect routing: zero and nonzero travel use the same comparison. This is prediction evidence only—no Unity editor execution, imported-asset readback, runtime playback, or engine certification occurs. The contract uses prediction provenance V6 and output V19; historical output V9 and other readers remain immutable. The checked-in/CI FBX inputs for this slice are self-authored synthetic fixtures.

Generating engine import advice

The separate historical revision-1 advice profile remains the route for projecting Convert Units, Bake Axis Conversion, Generic Root Motion Source, and the three ModelImporterClipAnimation.lockRoot* booleans. Use a revision-1 Unity advice config for this command:

$ animsmith --config unity-advice.animsmith.toml generate import-advice export.fbx \
    > export.import-advice.json

Unreal 5.8 and Godot 4.7 revision 1 deliberately exit 1 with profile_settings_unmodeled; their frozen profiles do not yet define importer settings. That command does not guess frame ranges, sample rates, unit conversion, or root-motion behavior.

Revision 2 provides the narrow document-setting projection for the exact Godot and Unreal tuples:

# Godot 4.7 / revision 2 / resource-importer-scene (glTF JSON or GLB)
[engine]
profile = "godot"
profile_revision = 2
engine_version = "4.7"
importer = "resource-importer-scene"

[engine.settings]
animation_fps = 30
animation_trimming = false

Godot's only projected keys are animation/fps (1..120, default 30) and animation/trimming (default false). Unreal 5.8 / revision 2 / fbx-importer accepts FBX and requires one explicit document setting:

[engine]
profile = "unreal"
profile_revision = 2
engine_version = "5.8"
importer = "fbx-importer"

[engine.settings]
sample_rate = "default_30" # or "source_determined" / "custom_hz(60)"

The V2 JSON contract is urn:animsmith:schema:engine-import-advice:2. It contains one available native import-setting projection or one typed refusal; it does not claim importer execution, frame ranges, units, skeleton/retargeting, compression, root motion, runtime behavior, or project-file changes.

Steering a run without a config

You can also shape a run from the command line. --select restricts the run set, --allow suppresses findings, and [checks.<id>] severity (including "off") reshapes how hard each check fails:

$ animsmith lint --select quat-norm examples/assets/clip-dirty.glb   # only quat-norm
$ animsmith lint --allow quat-flip examples/assets/clip-dirty.glb    # hide quat-flip

Demote a check while an upstream fix is pending (a [checks.quat-flip] severity = "note" override turns the warning into a note):

$ cat demote.toml
[checks.quat-flip]
severity = "note"

$ animsmith lint --config demote.toml examples/assets/clip-dirty.glb
  error[quat-norm] clip 'swing' bone 'spine' @0.500s: non-unit rotation key ...
  note[quat-flip] clip 'swing' bone 'spine' @0.750s: 2 hemisphere flip(s) ...
1 error(s), 0 warning(s), 1 note(s), 1 coverage gap(s), 0 available prediction facet(s), ...   # exits 1

See the configuration reference for the full key reference.


5. Converting exports and generating reports (default features only)

Use this workflow when a DCC or marketplace export reaches your importer but brings bloated constant tracks, stray scale keys, or a rig the retargeter cannot trust. That is the hygiene side of the file is bloated, or the retargeter chokes and the rig-contract side of a limb is T-posed, or a bone never moves: convert to glTF, measure/lint/report, then compare against the previous asset before committing the migration.

convert normalizes an FBX (or glTF) export into a clean glTF, and report renders a self-contained HTML report with skeleton playback and metric charts. Both are in the default build; a --no-default-features binary omits them. report --evidence-only writes the same findings, coverage gaps and engine predictions without the sampled pose grid — keeping the charts in a single-clip report, though not in a comparison, whose panels are drawn from that grid — for sharing evidence where the clip itself cannot travel.

$ animsmith convert export.fbx -o clip.glb
$ animsmith measure clip.glb
$ animsmith report clip.glb -o report.html
$ animsmith diff old.glb clip.glb

The repository also carries a byte-checked, self-authored visual-comparison pair. It exercises the report's acceptance context without a commercial or private source asset:

$ animsmith --config examples/report-comparison.animsmith.toml report \
    examples/assets/report-comparison-before.glb \
    --compare-after examples/assets/report-comparison-after.glb \
    --before-clip acceptance-matrix --after-clip acceptance-matrix \
    -o comparison.html

In comparison.html, select the before-side loop finding to see the exact first and last judged poses overlaid with the affected bone highlighted. The trajectory panels disclose root, hips, and bilateral-foot trails; the gait panel shades the typed foot-slide stance intervals and names the selected role. The combined root chart uses one metres scale and distinct before/after legends. The before-side constant-track row is explicitly structural evidence: its quaternion keys can be removed while the sampled poses look unchanged. Neither the fixture nor the report certifies artistic, gameplay, importer, or engine acceptance.

cargo run -p animsmith --example gen_example_assets regenerates both inputs. The example_assets_match_generator_output integration test rebuilds every committed example and compares its bytes, so CI catches hand edits or generator drift.

When one authoritative skinned base and its animation takes live in separate files, use the versioned character-assembly.toml recipe instead. assemble produces one GLB and evidence pair; source-package extraction and project publication stay outside animsmith.

The example uses current recipe v7 without its optional rest/bind operation. For glTF/GLB or inventory-complete normalized/baked FBX assembly, add [rest_bind_scale] with the required exact root_node_name and expected_factor fields. The base must resolve that name to one source node and exactly one non-empty skin whose every joint is that node or a descendant. Every distinct FBX clip is projected to its normalized skeleton and takes. A glTF/GLB clip retains its existing successful full rest/bind or meshless track-only path; its clip-track gate additionally admits only failures in unused geometry, deformation, material, or bind domains while retaining framing, dependency, raw-coverage, named-skeleton, and animation accessor/layout safety. The basis proof covers the selected base skin joints, exact root, their named ancestry, and actual track targets; unreferenced base-only geometry descendants need not appear in the clip. Every input passes basis compatibility before remapping.

$ animsmith assemble examples/character-assembly.toml \
    -o character.glb --evidence character.assembly.json

Getting a test asset

We do not ship third-party assets. To try this on a real rig:

  • Mixamo — free with an Adobe ID and royalty-free for personal and commercial use. Download a character + animation as FBX, then convert it. Mixamo is also a built-in rig profile, so [rig] profile = "mixamo" resolves its roles for the semantic checks. Check Adobe's current terms before redistributing any downloaded asset; the safe path is to keep them out of your repo. The Mixamo tutorial takes one download end-to-end, from this convert step through a contract config.
  • CC0 / public-domain sources for assets you want to commit.
  • Or generate your own — see the asset generator this repo uses for its own fixtures.

6. Embedding animsmith as a library gate

Use embedding when the team already has an importer or build pipeline and wants animsmith's checks as a library step instead of a separate shell command. The pipeline-library use case is the payoff in why animsmith: load your asset once, map your own contract into Config, and surface findings in the same gate that owns the rest of your asset rules.

Pipelines can skip the CLI and drive the check catalog directly: load a document, resolve rig roles, build a Config from your own contract format, measure, run the checks, and map findings to your gate.

The runnable walkthrough is crates/animsmith/examples/embed.rs, paired with embedding.md:

$ cargo run -p animsmith --example embed

It exits 1 on purpose — the example declares a deliberately wrong expectation to demonstrate a failing gate, not an accidental error.


Feature matrix

ExampleNeeds
1 first gate, 2 repair, 3 transform, 4 configany build (incl. --no-default-features)
5 convert / reportdefault features (fbx, report)
6 embeddinglibrary crates

Asset policy

Assets committed to this repo are procedurally generated or CC0, with provenance recorded. Third-party assets (Mixamo and similar) are used via documented download steps, not checked-in bytes or download scripts, unless their terms clearly permit redistribution.

Advanced workflows

  • Static asset workflow guide — Diagnose bounds and transform domains, preserve normal maps, bake supported static placement, attach explicit textures, and understand what still needs engine validation.
  • Scaling glTF safely — Choose whole-document unit conversion or rest/bind reparameterization, understand the exact-source rewrite/proof transaction, and interpret its support boundary.
  • Material texture recipes — Attach explicit BaseColor, normal, metallic-roughness, and occlusion images during conversion with deterministic resizing and provenance evidence.
  • Multi-source character assembly — Combine an authoritative skinned base with exact takes and timeline windows from separate inputs, producing one deterministic GLB plus evidence.
  • Collection contract extensions — Read the contact-fragment, foot-cycle planner, and transition-family contracts, including strict manifest and contact-evidence bindings.

Static assets: bounds, transforms, and textures

This guide is for artists, technical artists, engine integrators, and build engineers handing a prop or environment asset from a DCC tool to a game. It explains four questions that look similar in a viewport but need different answers in an asset pipeline:

  1. Which bounding box are we measuring?
  2. Is placement stored in mesh vertices or in a node hierarchy?
  3. Did the normal map survive conversion as a normal map?
  4. Should missing texture links be preserved, repaired declaratively, or sent back to the artist?

animsmith reports or performs only the part it can prove. It does not certify an engine's import settings, generate artistic material content, or replace a runtime smoke test.

Start with the handoff problem

What you observeLikely sourceStart with
The prop is the right shape but has the wrong size or placement after import.Object or parent transforms survived as scene hierarchy instead of being applied to vertices.animsmith measure; compare definition, instance, and scene bounds.
Culling, framing, or placement code sees a different box than the DCC viewport.The code and artist are discussing different coordinate domains.animsmith measure --format json.
The surface silhouette is right but grooves and bevels look flat.The normal image or material slot was lost, or the engine imported it as color data.Ordinary convert, then inspect the material in the engine.
The model is gray or flat because exported material links are incomplete.External image paths were not portable or the exporter omitted them.convert --material-texture-recipe.
You need to know which material slot uses which source image before an engine import.A mesh view alone hides material-to-texture-to-image sharing and image encoding facts.animsmith measure --format json.
A consumer requires identity transforms and mesh-local final geometry.The consumer does not retain or intentionally does not evaluate the source node hierarchy.convert --bake-static-mesh-transforms.

Which bounding box do you mean?

A mesh definition contains reusable vertex data. A node places an instance of that mesh into a hierarchy. A scene chooses root nodes and therefore a set of instances. The glTF scene and node model and its transformation rules make those identities separate: a mesh's global transform comes from the node that references it, and a child inherits its parents' transforms.

That gives one asset several valid axis-aligned bounding boxes (AABBs):

animsmith recordCoordinate domainQuestion it answers
mesh_definitions[].geometry_aabbMesh-local source positionsHow large is the reusable geometry before any placement?
mesh_definitions[].geometry_centroidMesh-local finite source positionsWhere is the average authored vertex position before any placement?
node_instances[].static_node_world_aabbDefault/rest hierarchyWhere and how large is this particular static instance after its parent and local transforms?
scenes[].static_scene_world_aabbOne declared source sceneWhat box encloses the measurable static instances reachable from this scene's roots?

For example, a unit cube can have a mesh-local box from (0, 0, 0) to (1, 1, 1), while a node with scale 2 and translation 10 along X has a static node-world box from (10, 0, 0) to (12, 2, 2). Neither result is wrong. Using the first as a placed-world bound, or the second as a reusable mesh bound, is the bug.

geometry_centroid is a separate mesh-local fact: it is the arithmetic mean of finite decoded base POSITION rows, not the center of geometry_aabb. An asymmetric mesh can therefore have a centroid away from its box center. It is useful for checking authored pivots or coarse placement assumptions, but it is not a center of mass, surface-area-weighted center, collision origin, or a runtime placement guarantee.

For indexed geometry, each stored POSITION contributes once even when the index stream references it repeatedly; for unindexed geometry, each stored position contributes once. Empty or wholly non-finite geometry has no centroid. animsmith reports this evidence but does not move the pivot, recenter the mesh, or bake transforms as part of measure.

animsmith measure prop.glb
animsmith measure prop.glb --format json > prop.measure.json

Why the distinction matters

  • Artists can tell whether unexpected size comes from modeled geometry or an unapplied object/parent transform.
  • Engine integrators can choose the domain their importer, preview camera, culling system, or placement code actually consumes.
  • Pipeline developers can compare the same source identities across re-exports instead of joining records by non-unique display names.

An unavailable box is not an empty or zero-sized box. Skinned deformation, non-finite transforms, or a definition without finite positions produce an explicit unavailability reason. Scene records also expose excluded instance counts, so a partial static union cannot masquerade as complete coverage.

These are default/rest source-asset measurements. They deliberately exclude animation, skin and morph deformation, runtime component transforms, engine-generated collision, and camera- or frame-dependent bounds. Validate those in the consuming engine.

Inventory material and image handoffs

Before treating a missing detail as an engine-shader problem, inspect the source material graph:

animsmith measure prop.glb --format json > prop.measure.json

For glTF/GLB, the nested measurement data records source-order material definitions and their semantic bindings, then texture-to-image references and image metadata. That exposes, for example, a normal slot that points to the same image as BaseColor, a texture shared by several materials, a linked external image, or an image whose declared MIME disagrees with the container found in its bytes. Successful image inspection reports decoded width, height, channel count, and color type. It does not decide whether a BaseColor texture is authored well, whether a normal map has the intended convention, or whether an engine importer will choose the right settings.

An unavailable image is explicit rather than silently omitted: it has an unavailability reason and no decoded metadata. A malformed PNG/JPEG may still identify its container, but is not repaired. Other loaders can report material_resource_coverage: "unavailable"; do not equate that with an empty material table. This inventory is source evidence only. It neither resizes or transcodes images nor makes a later writer or conversion recipe authoritative for every original resource. Keep the raw handoff asset when byte-level source provenance matters.

Preserve normal maps as data, not color

A tangent-space normal map stores direction vectors in RGB texels. It changes lighting without changing the mesh silhouette; Unity's normal-map introduction shows why a low-poly surface can retain grooves, rivets, and other fine detail. Blender likewise documents that a tangent-space Normal Map node expects matching UVs and non-color image data.

Ordinary full-scene conversion preserves available linked or embedded PNG/JPEG base-color and normal images. A glTF normal texture keeps its normalTexture.scale; an FBX normal map uses glTF's default scale because the ordinary FBX material does not expose the same scalar. The glTF material model defines the normal slot and scale separately from the sRGB BaseColor slot.

animsmith convert source.fbx -o source.glb

Expected result: a material that had a discoverable normal image still has a normal texture after conversion, and converting or transforming the glTF again does not silently replace it with the BaseColor image or discard its strength. If the original bytes already satisfy the operation, they pass through without being decoded and re-encoded.

What preservation does not fix

animsmith does not decide whether an image is artistically correct, convert a height map into a normal map, flip an engine-specific green-channel convention, generate missing tangents, or configure the engine's texture importer. For example, Unity still requires the texture to be imported as a normal map and documents its expected Y+ convention. A model can therefore contain the right glTF material link and still look wrong under engine-specific import or shader settings.

Bake static placement into geometry

Use --bake-static-mesh-transforms when a static consumer needs final placement in mesh-local vertices beneath identity nodes. A common example is a prop modeled under rotated or scaled helper objects: it looks right in the DCC and a hierarchy-aware viewer, but a consumer that extracts only the mesh definition gets the unplaced shape.

animsmith convert prop.fbx -o prop-baked.glb \
  --bake-static-mesh-transforms

The operation accumulates the accepted rest hierarchy into positions, applies the inverse-transpose transform to normals and normalizes them, then emits a canonical identity root with identity mesh children. It retains indices, UVs, supported material assignments, BaseColor textures, and normal textures. convert --format json records the source identity, applied world matrix, determinant, and output identity for each baked instance.

Conceptually, this is the exchange-format counterpart of applying object transforms in a DCC. Blender's Apply transforms documentation explains the artist-side operation: transfer the current object transform into its data while keeping the visible result in place. animsmith's operation is intentionally narrower and fail-closed.

BeforeAfter
Placement may be split across parents and a mesh node.Accepted placement is in vertex positions.
Mesh nodes may have non-identity rest transforms.Output root and mesh children are identity transforms.
A hierarchy-aware viewer is required to reproduce placement.A consumer of the emitted mesh-local geometry sees the baked placement.
Source normals are local to the original mesh.Normals are inverse-transpose transformed and unit-normalized.

Rejection is part of the contract

The bake exits with an asset refusal (1) and writes no output instead of guessing when the source cannot be transformed without violating its preservation promises. The CLI reference owns the exact acceptance and rejection list.

It is not a general scene flattener. It does not bake skins or animation, duplicate shared instances, repair winding, preserve artistic pivots or helper hierarchies, choose a project-specific forward axis, or generate collision. Use the DCC or an engine-aware asset build step when one of those changes is intentional.

Attach textures with a recipe

Use --material-texture-recipe when the source material names are trustworthy but image links are missing, non-portable, or subject to a project size cap. Do not add a recipe merely because conversion supports one: ordinary conversion already preserves texture links it can discover.

animsmith convert prop.fbx -o prop.glb \
  --material-texture-recipe recipes/prop-materials.toml

A typical bad handoff has a material named surface, an artist-approved BaseColor image and tangent-space normal image beside the source, but an FBX whose external paths point to another workstation. A recipe maps the exact, case-sensitive source material name to those two images. If either path, image, or material match is invalid, the whole conversion fails before output.

The successful result has one explicit BaseColor/normal pair for every recipe entry. Images within max_dimension keep their original bytes and MIME type. Larger images use deterministic, role-aware processing: BaseColor is treated as color, while a normal map is treated as vector data and renormalized. Conversion evidence records every consumed and emitted image.

The recipe does not generate textures, invent material matches, pack ORM maps, add unsupported PBR slots, repair UVs or tangents, change normal-map handedness, or judge artistic quality. See the material texture recipe reference for the exact TOML schema, containment policy, limits, and processing contract.

Verify the result in layers

One successful conversion is evidence about the asset operation, not proof of the whole runtime presentation. Keep the raw source immutable and verify each layer explicitly:

# 1. Record source bounds and identities.
animsmith measure prop.fbx --format json > prop.before.json

# 2. Convert with only the operations your handoff requires.
animsmith convert prop.fbx -o prop.glb \
  --bake-static-mesh-transforms \
  --material-texture-recipe recipes/prop-materials.toml \
  --format json > prop.conversion.json

# 3. Measure the written artifact, not only the source.
animsmith measure prop.glb --format json > prop.after.json

Then inspect the written artifact in the target engine:

  • confirm size, forward/up orientation, pivot expectations, and hierarchy;
  • confirm the shader uses BaseColor and normal textures in the intended slots;
  • confirm the normal texture's import type and channel convention;
  • exercise runtime culling, LODs, collision, lighting, and instancing;
  • retain the conversion evidence beside the derived artifact so automation can explain how it was produced.

That final engine pass stays necessary because animsmith does not know the consumer's shaders, import presets, physics scale, collision generation, or runtime placement policy.

Scaling safely

animsmith scale is an evidence-emitting producer for two different scale operations. glTF/GLB supports both; the narrow FBX boundary supports only rest/bind and always emits a new GLB. Neither operation guesses units or scale policy from geometry, names, or asset categories.

Use the CLI reference for the exact command grammar and machine-readable output for the scale-evidence v4/v5 schemas. This guide owns the operator workflow and the boundary between the two operations.

Choose the operation

OperationUse it when…Physical result
whole-documentThe entire source uses the wrong linear unit.Every represented length changes by the declared factor.
rest-bindWorld geometry is already correct, but one skinned hierarchy carries a compensating inherited uniform scale.World joint translations and orientations, sampled trajectories, and skinned geometry are preserved while the hierarchy is reparameterized to remove the composed scale.

These operations are not interchangeable. A rest/bind cleanup must not be selected merely because a character appears too large or small, and a unit conversion must not be used to remove an inherited scale from one hierarchy.

Whole-document conversion takes one finite positive --factor. Rest/bind requires all three declarations: a raw source-skin index, a raw source-root node index, and the finite positive factor expected at that root. Use animsmith measure source.glb to list authored source-node and source-skin indices; neither selector is a normalized bone id or mesh-instance ordinal.

animsmith scale whole-document source.glb -o metres.glb \
  --factor 0.01 --evidence metres.scale.json

animsmith scale rest-bind source.glb -o canonical.glb \
  --source-skin-index 0 --source-root-node-index 3 \
  --expected-factor 0.01 --evidence canonical.scale.json

There is no in-place mode, plan file, animsmith.toml key, implicit first skin/root, or per-run tolerance override. Input, artifact, and evidence paths must be three distinct files. For narrow FBX rest-bind, neither destination may name a safe dependency key retained by the same-load closure, and a symlink-refused dependency or terminal closure-budget stop prevents publication. glTF/GLB artifacts keep the input container; narrow FBX rest-bind emits a new .glb.

What one run proves

One invocation performs one ordered transaction:

  1. Parse the source and build a complete capability inventory.
  2. Compile one format-neutral ScalePlan containing the exact topology, payload shape, field ownership, and proof claims for that source.
  3. For glTF/GLB, apply that plan directly to source JSON and buffer bytes; the normalized model writer is not used. Narrow FBX rest-bind first writes a private normalized GLB, then applies the plan and proof to that GLB.
  4. Reload the exact emitted bytes and run the independent normalized core proof plus the raw artifact proof. The artifact proof also reruns the writer and requires deterministic bytes.
  5. Stage the artifact/evidence pair, read the staged artifact back, and require its digest to match the bytes that were proved before promoting both files.

A reported refusal publishes neither file and restores any prior pair. The two destinations require two renames, so a process killed between them has the crash window documented in machine-readable output. Proof is not run again after staging: digest equality binds the already-proved bytes to the published file.

The current fixed tolerance identity is appendix-d-v6. It is recorded in scale evidence together with each evaluated residual and both observed-factor witness fields. Rest/bind derives those witnesses independently from the raw source projection and normalized skeleton; whole-document conversion records the declared factor in both because that operation has no source factor to measure. There is no runtime alias for an older policy and no per-run tolerance knob.

Write sets and preservation

Whole-document conversion rewrites represented lengths: node TRS translations and matrix translation columns, mesh POSITION, translation animation values and cubic tangents, raw glTF morph-target POSITION deltas, inverse-bind translation columns, and corresponding accessor bounds. Rotation, scale, normals, UVs, static and animated morph weights, key times, and other dimensionless payloads remain outside the write set. Static JSON morph weights retain their numeric values; animated weight accessor payloads remain byte-exact.

Rest/bind reparameterization derives every multiplier from the selected raw topology. It rewrites the necessary node-local translation/scale or matrix components, translation and scale animation payload, and affected inverse binds. Mesh positions and the already-correct world geometry remain unchanged.

At factor one, whole-document conversion plans no semantic writes. See the scale output contract for the exact artifact-preservation and evidence-field semantics at that boundary.

Supported source boundary

Scale accepts self-contained glTF/GLB for both operations. A .gltf source with an external buffer or image is refused rather than partially converted. Raw preflight also refuses any source domain the current model and artifact proof cannot preserve completely. Whole-document conversion admits only raw glTF POSITION morph-target deltas with dimensionless static/animated weights; NORMAL, TANGENT, sparse/interleaved scale-bearing accessors, unsafe aliases, and every morph payload under rest/bind reparameterization remain refused. It also refuses cameras and lights, along with GPU instancing, unregistered extensions, extras, non-triangle primitives, secondary skin-influence sets, unsafe accessor layouts, and animation targeting a matrix-authored node. The rejection record contains the complete typed violation inventory.

Do not read the core model's static unprojected-connector rows as a current glTF capability. The glTF loader projects every accepted node today, and the raw writer has no connector-bridge implementation; that path is outside the supported source boundary.

With the default FBX feature, scale rest-bind source.fbx -o canonical.glb accepts only the inventory-complete normalized subset. It requires the same explicit skin/root/factor declarations, stages a private normalized GLB, maps both selectors by exact unique named identity, then uses the existing raw GLB rewrite/reload/proof path. The same-load gate admits exact marker, LOD-group, stereo-camera, camera-switcher, and display-layer typed lists because none can supply rest/bind state to that bridge, plus shader/binding-table metadata. Display layers contribute only node membership and editor visibility/freeze/color state; they do not alter projected nodes. It admits a BindPose only when it covers every joint of each skin it touches and its converted rows are finite, unambiguous, and agree with the converted cluster bind or node rest-world matrix already consumed by the bridge; a source without a Pose remains valid. The raw aggregate still counts all admitted rows. Residual unmodeled typed lists fail closed with exact nonzero kind counts. The same-load gate may also admit enumerated scale-invariant conversion fidelity—omitted authored vertex/face/edge metadata, influence projection with complete effective coverage, triangulation, and exact-bit welding—without removing any inventory evidence. Missing influence coverage, omitted point/line or mesh geometry, unsupported skinning/deformers, incomplete binds or normals, and unclassified payload remain blockers. It records the full FBX inventory and staged GLB digest in scale-evidence v5. The published artifact is a re-encoded GLB: no raw FBX byte, object-property, marker/camera/display-layer behavior, or authored-curve-key preservation is claimed. FBX whole-document conversion remains refused. Character assembly likewise does not silently apply either scale operation. Its existing bind-pose canonicalization remains a distinct, explicit recipe operation. Morph support intentionally remains a raw glTF whole-document capability; it does not add morphs to the shared normalized model or enable rest/bind morph reparameterization.

Outcomes and evidence

The CLI reference owns exit-status semantics, and the output reference owns the scale-evidence v4/v5 wire contract and publication outcomes. In brief, --format json prints the same record that a successful run writes to --evidence; a refused run prints its rejection record but never writes the evidence destination because there is no artifact/evidence pair to publish. The record carries no timestamp; declared paths remain as entered, and identical inputs and arguments produce byte-identical evidence.

Embedding the same contract

Rust pipelines use animsmith_core::scale for planning and normalized proof, and animsmith-gltf for raw preflight, exact-source rewrite, reload, and artifact proof. Pass one compiled plan through rewrite_scale_plan; do not substitute the fixture-only analytic reference builder for a format writer. See the embedding guide and the crate rustdoc for exact type and error contracts. Appendix D in DESIGN.md is the normative algebra and preservation decision; the calibration notes record how the fixed proof policy is measured.

Material Texture Recipes

animsmith convert --material-texture-recipe <PATH> attaches explicit BaseColor, normal, metallic-roughness, and occlusion images to named source materials. It is for assets whose material links are incomplete or need a reproducible texture-size policy. The recipe is declarative: it does not generate artistic content.

animsmith convert input.fbx -o output.glb \
  --material-texture-recipe recipes/materials.toml

Recipe format

Recipes are TOML documents with immutable identity urn:animsmith:schema:material-texture-recipe:1. The retrievable JSON Schema is material-texture-recipe-v1.schema.json.

schema_version = 1
schema = "urn:animsmith:schema:material-texture-recipe:1"
texture_root = "textures"
max_dimension = 1024

[[materials]]
name = "surface"
base_color = "surface-base.png"
normal = "surface-normal.jpg"
metallic_roughness = "surface-metallic-roughness.png"
occlusion = "surface-occlusion.png"

materials is a list. Each entry requires one exact source-material name, one base_color path, and one normal path. metallic_roughness and occlusion are optional paths for their corresponding glTF PBR slots. Names are matched exactly, case-sensitively. A duplicate recipe name is an operator error because the recipe is intrinsically ambiguous. A valid recipe name that matches no source material, a source material omitted from the recipe, or an ambiguous source material is an asset-property refusal (exit 1): it describes the recipe's fit to the loaded asset rather than invalid syntax. Every declared entry must be used; a recipe cannot partially apply and succeed. Unknown recipe fields are rejected instead of ignored.

Run animsmith inspect <input> first and copy names from its top-level materials section; its quoted names use TOML-compatible escapes and can be copied directly into a recipe. The mesh instances section shows which material each primitive references. Ambiguous source names are marked and must be corrected in the authoring tool before a recipe can target them; inspect does not rename or merge materials.

max_dimension is required and must be from 1 through 4096. It bounds both the requested output size and accepted source dimensions. Source image files are limited to 64 MiB. Only PNG and JPEG files are accepted; their type is determined from file magic, not a filename extension.

Paths and containment

All paths are interpreted relative to the recipe file. Empty, absolute, drive-prefixed, and backslash spellings are rejected for consistent cross-platform behavior. texture_root is optional. When it is set, it too is recipe-relative and is a containment root: the converter canonicalizes the root and every image path and rejects traversal or symlink resolution outside the root. Missing paths, non-regular files, and escaped paths are operator errors. Without texture_root, ordinary recipe-relative resolution applies, including explicit .. components, and no containment root is declared.

Deterministic processing

The converter processes entries in source-material order and image slots in BaseColor-then-normal-then-metallic-roughness-then-occlusion order. Recipe declaration order does not affect the artifact or evidence. If an image already fits max_dimension, it is a no-op: its original encoded bytes and MIME type are preserved. A resized image is emitted as PNG RGBA8.

BaseColor resizing converts sRGB into linear light, premultiplies alpha, uses Lanczos3 filtering, then converts back to sRGB. Normal resizing decodes tangent-space vectors, uses Triangle filtering, and renormalizes vectors before encoding. Metallic-roughness and occlusion maps use linear-channel Triangle filtering: they are not treated as sRGB color or tangent-space vectors. For resized output, the pinned encoder uses Best compression and NoFilter. Producer evidence names the pinned primary packages image 0.25.10, png 0.18.1, and zune-jpeg 0.5.15; Cargo.lock pins their full dependency closure. Given the same supported-platform inputs, recipe, and tool version, this policy produces deterministic output bytes.

Evidence

With --format json, conversion evidence records the recipe path and root, the declared dimension cap, the locked processor identity, every consumed image, and every emitted texture. Records are ordered as described above. See machine-readable output for the versioned evidence contract.

Multi-source character assembly

animsmith assemble combines one authoritative skinned base with animation takes from separate FBX or glTF inputs. Use it when a delivery spreads clips across individual files or long master timelines but the runtime needs one GLB.

animsmith assemble character-assembly.toml \
  -o character.glb \
  --evidence character.assembly.json

The command is available in default builds with the fbx feature. It always writes a GLB and versioned JSON evidence as one publication pair. It prepares both files before replacing either destination and restores prior outputs if the second replacement fails.

This rollback contract covers every error returned by the command. No portable filesystem primitive can make two independently named destinations power-loss-atomic, so after abrupt process or machine termination the consumer must verify the evidence artifact digest before publication.

Boundary

Assembly owns format-generic scene, skin, animation, material, and GLB work. The consuming project still owns source-package extraction, gameplay naming and acceptance policy, cache/generation policy, provenance beyond assembly evidence, and publication. DCC automation is outside this command.

Recipe

Start from examples/character-assembly.toml:

schema_version = 7
schema = "urn:animsmith:schema:character-assembly-recipe:7"
input_root = "inputs"
base_input = "base-character.fbx"
mesh_instances = ["character-mesh"]
remove_nodes = ["sword-placeholder"]
complete_tracks = true
canonicalize_skin = true
ground_and_center = true
prune_constant_tracks = false
fps = 30.0

# Optional for glTF/GLB and the inventory-complete FBX subset; the exact root
# name and factor are required together.
# [rest_bind_scale]
# root_node_name = "root"
# expected_factor = 0.01

[[clips]]
name = "idle"
input = "idle.fbx"
take = "Take 001"
frame_window = [1, 61]
drop_closing_endpoint = true
strip_bones = ["motion-root"]

All input paths are relative to input_root, which is itself relative to the recipe. Absolute paths, parent traversal, symlink components, and paths that escape the canonical root are rejected. Output paths are command arguments so the recipe remains relocatable.

The input root is an operator-controlled snapshot: do not mutate or replace its entries concurrently with assembly. Symlink checks and reads are separate portable filesystem operations, so the command does not claim to defend against an actor racing the input tree while it runs.

Discover exact recipe names

Inspect the base before writing the recipe:

animsmith inspect inputs/base-character.fbx

Copy the quoted name after node in the mesh instances section exactly into mesh_instances; quoted names use TOML-compatible escapes. Each entry also shows its source node, referenced mesh, skinned status, and the material used by every primitive. The top-level materials section lists the exact, case-sensitive names required by a material texture recipe. Duplicate skeleton node or material names are marked ambiguous; assembly and material recipe validation reject ambiguous authored names instead of guessing. Multiple mesh instances attached to one uniquely named node are not ambiguous and are selected together. inspect only reports the authored scene—it does not rename nodes or materials, merge duplicates, or repair the asset.

The base supplies the authoritative skeleton, rest pose, meshes, skins, materials, and textures. mesh_instances names the exact base nodes whose mesh instances survive; omitting it retains every instance. An explicit node name must be unique; every mesh instance attached to that selected node survives. Unreferenced mesh and material definitions are pruned. For a recipe-v7 FBX base, assembly resolves this selection once against the captured FBX node names and carries the selected scene through its private GLB scale stage. Any holder names generated by that private serialization are not a second recipe namespace. When rest_bind_scale is enabled, an explicit selection that retains no skinned base instance refuses as an asset-recipe-mismatch at the selection boundary; its diagnostic names the recipe selection rather than the private staging artifact.

Each [[clips]] entry selects one exact source take, renames it, and remaps its tracks to the base skeleton by exact unique bone name. Missing referenced bones and ambiguous names fail rather than retarget motion. An entry may then apply:

  • frame_window = [START, END]: one-based, inclusive source frame endpoints at fps, or time_window = [START, END] in seconds;
  • drop_closing_endpoint = true: remove one final key from every channel;
  • hold_frames = N: duplicate the final pose after N / fps seconds;
  • gait_anchor = true: explicitly declare an in-place cyclic gait and run the measured gait-anchor transform using the selected animsmith.toml rig configuration. Assembly refuses before publication when the Root role (or Hips fallback) has missing/non-finite trajectory evidence, more than 1 cm of horizontal endpoint displacement, or more than 1° of yaw accumulation; no interior step is subtracted as an allowance. Every nonconstant channel the operation would rotate must contain exactly one key at each declared 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 evidence refuses, as do duplicate (bone, property) channels (including constant channels). Exact admitted f32 key times drive verification, and an integer-index permutation performs the rewrite; exempt constant tracks cannot influence its period or shift. The complete skeleton, resolved roles, and track shapes are validated first; declared frames × skeleton bones, declared frames × tracks, and maximum authored keys × skeleton bones are independently bounded at an inclusive 1,000,000 samples. Yaw uses f64 first/final headings plus counted full-turn crossings. At sample zero it chooses the local +Z, +Y, or +X basis axis with the greatest finite horizontal projection, in that deterministic tie order, and retains it for the entire proof. This supports different source-axis conventions without allowing a later axis switch to hide yaw; loss of the selected horizontal projection refuses. Error does not grow with segment count; four f32 successors at the inclusive 1 cm/1° caps cover only authored endpoint translation/quaternion quantization;
  • strip_bones = [...]: remove every TRS track for named base bones.

Optional rest/bind scale canonicalization

Recipe v7 can opt into the accepted rest/bind scale operation with one top-level block:

[rest_bind_scale]
root_node_name = "root"
expected_factor = 0.01

The block has no defaults: the exact normalized root-node name and finite positive expected factor are both required. In the captured base, the name must resolve 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-only input uses the track-only case described below. 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 geometry, material, deformation, or bind obligations that assembly never consumes would fail. Framing, parser, topology, dependency, raw-coverage, named-skeleton, and animation accessor/layout checks remain strict. Missing or repeated names and zero or multiple applicable base skins fail closed. Leading or trailing whitespace is invalid rather than silently trimmed; internal whitespace remains exact. No source-array index is reused across files. Recipe v7 accepts glTF/GLB and the narrow FBX subset admitted by the existing normalized/baked rest-bind capability boundary. That boundary requires a complete ufbx inventory for the normalized metre/Y-up hierarchy, derived bind evidence, rebuilt geometry, and baked animation. Its same-load gate may admit enumerated scale-invariant conversion fidelity, including omitted authored vertex/face/edge metadata, influence projection with complete effective coverage, triangulation, and exact-bit welding, while retaining every inventory fact. Incomplete source-skeleton projection, unclassified payload, missing effective influences or geometry, incomplete bind evidence, residual unmodeled source elements or extensions, incomplete resource/construct coverage, or an incompatible basis fails closed. The source-aware gate may admit user-defined properties and bounded external texture/video declarations because neither supplies rest/bind state to the normalized GLB bridge. It also admits exactly ufbx's marker, LOD-group, stereo-camera, camera-switcher, and display-layer typed lists: they cannot supply hierarchy transforms, skin binds, tracks, or geometry to that bridge. Display layers contribute only node membership and editor visibility/freeze/color state; they do not alter projected nodes. The gate admits shader/binding-table metadata on the same basis. A BindPose is admitted only when it covers every joint of each skin it touches and its converted rows are finite, unambiguous, and agree with the converted cluster bind or node rest-world matrix already consumed by the bridge; no Pose remains required. Those elements remain counted in the raw aggregate, while every residual unmodeled typed list fails closed and is named by kind and count in the refusal. Rooted capture retains supported linked texture bytes through the normalized artifact, and neither publication destination may name one of the safe source-relative dependency keys retained by that same-load closure, including a key whose capture was unavailable or refused. The command stops before publication when a retained key was refused as symlink-mediated rather than resolving through the link, or when the closure resource budget leaves a declaration tail unchecked. The frozen v7 evidence retains declaration counts in its capability inventory; same-load raw facts and closure remain loader-side rather than being added to that wire record. Admission does not claim raw FBX object-property, marker, camera, material, or texture-assignment preservation. A refusal identifies the exact failed source fact, semantic domain, or inventory counter. Nothing is inferred from filenames, bounds, character height, inverse-bind magnitudes, or source units. Recipe v6 remains immutable with its explicit source indices; recipes v4/v5 remain immutable and continue to reject FBX when rest_bind_scale is active.

Recipe v7 retains recipe v6's composition with canonicalize_skin, ground_and_center, and remove_nodes. It captures and validates the raw inputs first, applies the same mesh selection and canonical assembly transforms to the staged candidate and an independently source-rebased reference, then maps selectors by names and ordered selected joint topology after any index changes. It serializes the staged candidate, performs the raw rest-bind rewrite last, reloads those exact bytes once for proof, and atomically publishes only after the final clips match the rebased reference. Recipe v4 remains immutable and continues to reject that combination.

Before any clip keys are remapped or copied, assembly captures and validates the exact bytes of the base and every separately supplied clip. Each input must agree on the selected domain and factor, named parent topology, target local rest matrices and orientations, helper-node layout, and coordinate convention. Each input's own target paths and plan-owned effective factors are validated and fingerprinted; distinct clips need not contain identical target paths. A matching digest alone is not compatibility evidence. Topology, orientation, helper-layout, basis, selector, or factor disagreement rejects the complete operation before publication. Topology, resolved names, and the declared factor compare exactly. V7 compares the exact resolved root and ordered selected-skin joint names plus the named source/helper parent paths; format-local skin/node numbers remain per-input evidence. Each input's selector identity is derived from its accepted plan and source document; a declared/resolved disagreement rejects the operation. V6 retains exact raw-selector comparison. Rest translations, scales, and matrices use the named Appendix D tolerance policy, while quaternion orientation uses shortest-path angular distance, so equivalent q/-q spellings do not become false incompatibilities.

For a compatible clip, translation values and both translation tangents of a CUBICSPLINE track are rebased in the source basis before exact-name remapping. Assembly reuses the same validated scale request, compiled write-set, raw glTF rewrite, tolerance policy, and paired proof residuals as animsmith scale. The assembled artifact is serialized, reloaded from its exact staged bytes, and proved once before the artifact/evidence pair can be published.

complete_tracks = true fills absent TRS channels from the base rest pose for the union of selected skin joints and nodes targeted by any emitted clip. Per-clip strip_bones entries stay excluded. Rotation key values are unit-normalized and made hemisphere-consistent without changing cubic tangent sign relationships. When FBX scale staging produces an independent reference skeleton, completion targets are carried by exact unique bone name and resolved separately in each skeleton; numeric bone IDs are document-local. Each skeleton's own node-removal closure is excluded before completion.

canonicalize_skin = true bakes each skin's common geometry-to-world transform into positions and normals and rewrites inverse binds consistently. ground_and_center = true additionally places the bind-pose geometry on Y=0 and centers it in X/Z while translating skeleton roots and root-translation animation tracks by the same offset. Inconsistent skin transforms, singular normal transforms, or shared mesh definitions that require incompatible bakes are rejected.

prune_constant_tracks = true removes tracks that the existing core constant- track predicate proves constant, after completion, quaternion cleanup, and all other assembly transforms. This is all-property pruning: it can remove completion-generated (bone, property) coverage when the completed value is constant, so it may undo part of complete_tracks = true. The carve-out uses the effective output clip's animates_bones exact names; it never uses required_bones, so declared motion evidence is preserved without retaining unrelated tracks. For an independent scale reference, authoritative prune decisions and the final clip proof are projected by exact unique bone name and channel property rather than by document-local bone or track indices. The canonical example leaves pruning disabled; opt in only after reviewing the completed clip and the consumer's transition behavior. A consumer that does not explicitly reset an omitted property during a transition can retain the outgoing clip's value, so leave pruning disabled where dense transition coverage matters until property-scoped selection is available (see #401). Set it to false or omit it to retain every otherwise eligible track.

remove_nodes = [...] selects exact, case-sensitive names from the post-canonicalization base skeleton and removes each selected node together with its complete descendant subtree. Every name must resolve exactly once; an absent or ambiguous name fails rather than being skipped. Overlapping ancestor and descendant selections are allowed and their union is removed once, in original parent-before-child node order. Selecting the entire skeleton is refused. Surviving descendants are never reparented because every descendant of a removed node belongs to the removal closure.

Removal is planned before track completion, and closure nodes are excluded from completion targets. The structural projection is applied only after clip processing and constant-track pruning. It is refused if a final track still targets the closure, if a skinned mesh instance is attached to it, or if a selected node remains referenced by a skin joint or complete source-skin identity. For an FBX input with rest_bind_scale, an unskinned mesh instance attached inside the declared closure is excluded from the private normalized scale stage and removed with that closure. The same instance still blocks scale when its node is not declared for removal. This is the only attachment reference consumed implicitly; per-clip strip_bones and constant-track pruning remain necessary to make an animated decorative subtree removable. The effective clip's animates_bones configuration still protects named tracks from pruning, so a protected surviving track causes removal to fail. [rig] required_bones is a lint presence policy, not a node-removal selector or protection list.

Errors are deterministic and no output is published on any refusal. A valid recipe that does not fit loaded asset facts is a typed asset refusal (exit 1); JSON mode emits producer-refusal:1, while text mode reports the same stable kind on stderr. Recipe syntax/schema/value, input/path, and publication failures are operator errors (exit 2) and emit no stdout. Recipe schema and value validation precede input loading. After the base's selected mesh and canonicalization steps, selectors and their descendant closure are resolved deterministically and the whole-skeleton rule is checked. After all clip transforms, every surviving track, mesh attachment, skin reference, and complete source-skin fact is checked before the hierarchy is changed. The reported refusal identifies the offending stored record; traversal order is an implementation detail rather than a wire contract.

Accepted projection preserves the original order and parent links of every surviving node, remaps all surviving node references through one stable map, and clears optional source-native skeleton projection because it can no longer describe the authored source completely. Node removal performs no mesh, material, or texture garbage collection. Outside the bounded FBX scale composition above, a mesh instance inside the closure is refused. The earlier explicit mesh_instances selection and its existing resource pruning remain the only assembly resource-garbage-collection pass. Evidence counts an unskinned instance consumed by declared node removal in transforms.removed_mesh_instances and omits its node name from retained_mesh_instances.

An optional material_texture_recipe reuses the exact material-name recipe boundary described in Material texture recipes, including BaseColor, normal, metallic-roughness, and occlusion slots.

The normative current recipe schema is character-assembly-recipe-v7.schema.json. Recipe v1 through v6 remain immutable historical contracts. Migrate a v6 rest/bind recipe by changing its identity to v7 and replacing both source-index fields with root_node_name. Omitting the block remains ordinary assembly behavior; v3 continues to reject the block as unknown.

Evidence and determinism

The current evidence identity is urn:animsmith:schema:character-assembly-evidence:7. It records the effective recipe, recipe and input SHA-256 digests, the selected configuration file's declared path and digest (or an explicit built-in-defaults marker), selected takes and windows, exact source/base bone remap names and indices, and track operation counts, start/end/delta facts for named translation tracks removed by strip_bones, mesh selection, canonicalization flags, tool identity, and the final artifact digest and counts. When pruning is enabled, each clip records the exact removed tracks in pruned_constant_tracks: the index in the completed, normalized output clip immediately before pruning (retaining its pre-prune authored order), exact bone name and bone_index in the post-canonicalization/pre-node-removal skeleton, TRS property, interpolation, and key count. bone_remaps.base_index uses that same pre-removal coordinate space. Consumers derive a surviving final index by subtracting earlier transforms.removed_nodes[].original_node_index entries; a pruned track whose node is later removed intentionally has no final-artifact node index. The array is empty when pruning is disabled or no track is removed. The top-level transforms.removed_nodes array records every projected node exactly once in original pre-removal node order: its exact name, original node index, nullable original parent index, and whether the recipe selected it directly rather than through an ancestor. It is empty when remove_nodes is omitted or empty. See character-assembly-evidence-v7.schema.json.

When rest_bind_scale is active, v7 pins each exact base/clip input digest and its versioned basis fingerprint, the declared root name, each input's resolved root name/source-node index, the source-skin index for full rest/bind inputs, and the effective staged selectors after canonicalization/removal, the factor, every compatibility result, and the shared final-artifact scale proof. The staged-source digest and exact emitted-byte read-back digest make the two serialization boundaries explicit; the read-back digest equals the published artifact identity on every accepted run. Each input records application = rest-bind with fingerprint contract urn:animsmith:character-assembly-scale-basis:1, or application = skinless-clip-tracks with urn:animsmith:character-assembly-skinless-clip-scale-basis:1, so consumers never infer the applied domain or version from the digest. The operation record also carries the sibling residual_comparison_counts record pairing every shared maximum residual to its comparison count by the same stable field name. These records let a consumer verify that assembly validated and transformed every participating input before remapping and that proof consumed the exact bytes subsequently published.

V7 constructs an explicit animation-only projection for each distinct FBX clip-only input and for each glTF/GLB clip admitted to the track-only path. The projection retains complete source-skeleton node coverage and every normalized take, while excluding source meshes, mesh instances, materials, skins, and inverse-bind state that cannot contribute to assembly. The captured input digest and source-projection evidence still describe the unmodified source, including unsupported FBX deformation or glTF bind payload. For glTF/GLB, the role-specific raw gate excludes only obligations wholly owned by those projected domains; malformed animation accessors, matrix-target animation, unsafe dependencies, and incomplete raw construct coverage still refuse. The clip projection's exact named root, every joint in the selected base skin, and the named ancestors connecting that rig domain must match the accepted skinned base basis in normalized topology. Rest translation, rotation, and scale remain strict independently for every node/property that any take omits; a component may differ only when every take explicitly authors that named channel, so assembly never falls back to the differing clip rest. Each actual clip track target and its named ancestors join that compatibility domain, and the target must belong to the base plan's affected closure. Base-only geometry and attachment descendants do not become clip requirements merely because the base plan rewrites them. The base plan supplies each named translation/scale target factor; cubic-spline in/value/out triplets are rebased together. A skinless base still refuses, as does any clip projection with incomplete source coverage, an incompatible named basis, or a track outside the base scale domain. Bind matrices are neither synthesized nor claimed for the track-only row. The retained skinless-clip-tracks label names the projected operation domain, not a requirement that the captured source was skinless. The exact final assembled artifact remains covered by the shared skinned-base rewrite and proof. Independently serialized animation-only FBX clip projections are compared under that proof's Appendix-D numeric tolerance; full rest/bind inputs retain exact pre-remap oracle comparison.

Each v7 scale-input row also identifies the captured container and its source projection. raw-gltf records authored-curve and raw-span preservation. normalized-baked-fbx explicitly records both as false, embeds the complete FBX capability inventory, and binds the private normalized GLB stage by digest and byte count. When declared node removal consumes an unskinned instance, that private stage excludes the instance while the raw input digest and capability inventory remain unchanged. This stage is not a second public artifact or evidence chain: assembly still serializes, rewrites, reloads, proves, digest-checks, and atomically publishes one final GLB/evidence pair.

Evidence v1 through v6 remain immutable historical contracts; v1 does not describe pruning, v1 and v2 do not describe structural node removal, and none of them describe rest/bind scale compatibility. Consumers migrating from v2 or v3 must verify removed_nodes against the effective recipe and output hierarchy rather than inferring removal from track evidence or lint configuration.

Given identical recipe bytes, input and config bytes, tool build, paths, and platform, repeated runs emit byte-identical GLB and evidence. Evidence keeps operator-declared paths rather than leaking canonical host paths.

Collection contract extensions

This document records versioned extensions to the file-scoped collection decision in DESIGN.md Appendix F. Contact-fragment V1 includes its strict standalone and manifest-selected producer/CLI/publication boundary. The directional-speed policy/evaluation V1 is a separate ordered 0.6.0 slice with one JSON-only evaluator command. The contact-transform result and foot-cycle map planner are format-neutral contracts; animation rewriting, collection publication, and runtime systems remain separate work.

Directional-speed policy V1 (#552, ordered slice)

The strict collection-directional-speed-policy:1 reader is an ordered, manifest-bound declaration for the directional-speed evaluator. It is a separate TOML envelope: it does not add fields to collection-manifest V1, revise collection-output V2, or infer membership from filenames or paths. animsmith collection evaluate-directional-speed --policy POLICY.toml --evidence COLLECTION-OUTPUT.json --format json binds its raw bounded inputs to the typed reader, strict current collection-output V3 adapter, pure evaluator, and immutable result. There is no output file, text/Markdown, subset, or inference mode; publication remains a consumer responsibility.

The envelope repeats the exact manifest identity (collection_id plus {sha256, bytes}), one directional-blend runtime_set_id, and every existing logical member exactly once in manifest order. Each member has one nonzero, unique semantic [x, z] coordinate. source_basis.x and source_basis.z are orientation witnesses for raw collection-output V3 +X/+Z endpoint displacement in that semantic plane; their magnitudes are nonsemantic, and the evaluator uses unit axes for heading. They must be finite, bounded, nonzero, and perpendicular. diagonal_behavior is closed and applies to unit-input/base targets: for coordinate c, preserve uses gain g(c) = hypot(c) while normalize uses g(c) = 1. Thus uniform expected speed is base * g(c), authored expected speed is the member's base speed_mps * g(c), and ratios compare expected_measured_ratio_i_to_ref = declared_expected_ratio_i * g(c_i) / g(c_ref), with the reference declaration fixed at 1.0. Direction comparison is unaffected by this gain.

The declaration also carries finite bounded direction_tolerance_deg in the inclusive range 0..=180 degrees. The pure evaluator normalizes raw endpoint displacement before source-basis projection and uses published speed_mps for magnitude (not travel). Its immutable result binds raw policy and evidence InputIdentity values and retains a manifest-ordered row for every member: policy coordinate, raw evidence, projected heading, comparison values, tolerances, deviations, and pass/violation outcome. Incomplete root travel, zero endpoint, zero ratio reference, and numeric range are typed not-evaluated outcomes, never implicit passes, failures, or subsets.

uniform requires uniform_speed_mps and speed_tolerance_mps; authored requires each member's speed_mps plus speed_tolerance_mps; and ratios requires a declared reference_member, each member's expected_ratio, and a dimensionless ratio_tolerance. Mode-inapplicable fields are rejected. The strict reader rejects parse/declaration failures including unknown or duplicate fields, unknown tokens, malformed identities, duplicate member ids or coordinates, invalid coordinates or basis, nonfinite/out-of-range values, and N+1 members before retaining them. Separate binding validation rejects stale manifest identity, wrong runtime-set id or kind, and missing, extra, or reordered member sequences. No licensed asset data belongs in this declaration or its tests.

Foot-cycle parameterization V1 (#18, planner slice)

urn:animsmith:schema:foot-cycle-parameterization:1 is a strict bounded TOML declaration for one existing collection-manifest gait-group. It repeats the exact manifest identity, runtime-set id, exact ordered members, one explicit reference member, one safe contact-fragment path per member, finite positive minimum/maximum segment slopes whose inclusive interval contains 1.0, and one safe future output-directory path. The reference owns canonical contact-boundary phases. Membership, ordering, contact paths, and reference ownership are never inferred from filenames or clip names.

schema = "urn:animsmith:schema:foot-cycle-parameterization:1"
schema_version = 1
runtime_set_id = "com.example/sets/walk"
reference_member = "com.example/walk-forward"
output_directory = "generated/walk-aligned"
minimum_segment_slope = 0.5
maximum_segment_slope = 2.0

[proof]
max_gait_phase_spread = 0.08
min_lr_amplitude_m = 0.05
max_contact_boundary_phase_error = 0.01

[manifest]
schema = "urn:animsmith:schema:collection-manifest:1"
schema_version = 1
collection_id = "com.example"

[manifest.input]
sha256 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
bytes = 1234

[[members]]
id = "com.example/walk-forward"
contact_fragment = "contacts/walk-forward.json"

[[members]]
id = "com.example/walk-right"
contact_fragment = "contacts/walk-right.json"

The reader is closed and bounded at 8 MiB and 4,096 members. Planning admits at most 16,384 contact events and 32 MiB of canonical contact-fragment bytes across the whole declared ring, so independent per-fragment limits cannot multiply into unbounded retained topology or canonicalization work. Exact supplied byte counts are preflighted before canonicalization; a false count is then caught by exact canonical byte-count and digest comparison on the first such row. Safe locators use the same lexical relative-path contract as collection manifests. It rejects unknown fields, unsupported identities/versions, unsafe or duplicate paths, duplicate/missing members, an absent reference, invalid manifest identities, and invalid slope ranges. The required closed [proof] table declares finite max_gait_phase_spread and max_contact_boundary_phase_error values in the inclusive range [0, 0.5] plus a finite non-negative min_lr_amplitude_m. These exact values are bound by the parameterization byte identity and retained in the plan; V1 has no defaults, gait-group/config lookup, filename inference, or cross-member policy merge. The published foot-cycle-parameterization-v1.schema.json describes the decoded TOML shape. Host canonicalization, symlink/alias detection, and the guarantee that output_directory is a previously absent sibling generation destination are enforced by the private source-preparation adapter described below; this parser performs no filesystem I/O.

The pure core planner additionally requires each supplied sidecar byte identity to equal its canonical contact-fragment:1 identity and its collection clip witness to equal the manifest's exact logical/source/take-index/take-name row. Each member also supplies independently measured typed Root/Hips evidence bound to the fragment's exact artifact, dependency closure, and collection clip source/take witness. Cross-wired or stale measurements refuse before their values are used. A missing, ambiguous, malformed, or non-finite witness refuses. Evidence retains signed endpoint X/Z displacement and signed accumulated unwrapped yaw. The unchanged admission derives horizontal displacement with binary64 hypot(X, Z) and uses absolute yaw; values above 0.01 m or 1 degree refuse, and both thresholds are inclusive. The planner consumes and retains those facts but does not sample an animation asset itself. V1 recognizes exactly AnimSmith's contact-support-detector:1 extension and validates its closed algorithm, sampling, frame-cap, threshold, and selected left/right foot-or-toe role provenance. Every member must carry the exact same finite non-negative contact_height_m binary64 value because that policy determines the compared window boundaries. Other extensions refuse because no operation-specific transform handler has run.

The private CLI-crate preparation adapter now bounded-reads the exact manifest and parameterization, resolves the exact member-reachable source/config subset in canonical manifest source-key order plus every declared contact fragment and absent output directory under its declaration root, and rejects symlinks, canonical collisions, and (where the platform exposes file identity) hardlink aliases before source parsing. It first validates the pure exact runtime-set binding, then loads only its member-reachable manifest sources/configs in manifest order; unrelated source rows do not gate this scoped operation. It captures each distinct canonical config once and shares that immutable normalized snapshot and explicit byte identity across its sources for later proof without a control-file reread. Distinct retained config snapshots share an inclusive 32 MiB invocation ceiling; repeated use of the same canonical config is counted and captured once. It requires complete dependency-closure identity and exact raw take index/name witnesses, counts primary, every retained external dependency identity, and normalized document semantic payload against per-source and invocation byte ceilings. Each loaded document passes strict structural shape validation before sampling or candidate work. The adapter then preflights both frames * bones pose cells and frames * tracks sampling work for every selected member and admits the complete multi-member totals against the inclusive one-million-cell/work ceiling before constructing the first metric grid. The prepared in-memory collection retains those exact source pose-cell and sample-evaluation totals so a later output proof can share one invocation budget without resampling or rereading controls. After planning, it runs the core clip-candidate preflight for every member and checks invocation-wide candidate-key, retained-value, exact candidate-storage-byte (including name), and work totals before constructing the first candidate. It then derives Root/Hips endpoint X/Z hypot and absolute unwrapped yaw through the existing metric grid/role/trajectory authority, and cross-checks every returned plan binding and duration before calling time_warp_clip_v1. Any refusal drops the whole in-memory batch and creates no output or partial result.

Preparation deliberately stops contact transformation at a typed continuation. It validates and reconstructs the closed stance-detector payload through the operation-specific core handler, but a fresh output artifact identity and its dependency closure do not exist until the candidate document is serialized. Only that later serialization step may supply those exact identities and call transform_contact_fragment_v1; preparation does not hash an in-memory Clip, claim a generated artifact identity, serialize, reread/prove, or publish. No user-visible command is added by this slice. A freshly serialized exact-copy candidate may truthfully retain the input content identity; freshness comes from capturing the serialization result, not from requiring unequal digests.

Each member must contain complete positive left/right support windows, each with exactly one same-side marker. Fragment rows stay physically linear: windows are partitioned by side, and same-side runs must be strictly disjoint (even touching same-side runs refuse). When one side has both a first run starting at 0 and a distinct last run ending at 1, those two physical rows form one logical cyclic stance. Their artificial seam edges are omitted from topology correspondence, while both windows and both markers remain in the transformed fragment. A single full-cycle [0,1] stance refuses. Opposite-side windows may overlap or touch. After seam coalescing, both sides must have the same nonzero logical-run count and their onsets must alternate around the cycle.

The planner derives true logical onsets/releases in deterministic cyclic chronological order, including a fixed kind order for simultaneous phases. It rotates only the correspondence sequence to a true left-support onset; it does not rotate numeric clip phases. Therefore the reference boundary phase remains its authored normalized phase and a member that would require moving phase zero, including incompatible wrap/non-wrap correspondence, refuses as non-monotone.

Independent output proof first requires the expected and freshly detected (side, edge) sequence in absolute phase-zero chronological order to match. It then compares those phase-zero-paired boundary phases with circular numeric distance, but a complete left/right role swap is a topology failure even at the maximum allowed boundary-error tolerance.

For matching signatures, corresponding source onsets/releases map to the reference phases. Simultaneous source boundaries are admitted only when all corresponding reference boundaries are simultaneous at one phase, and vice versa. Identical (source, output) pairs collapse to one map knot; a group split on either axis refuses. (0,0) and (1,1) are then included in that same deduplication, so the planner preflights the 4,096-point contact-transform cap from the retained knots rather than raw boundary rows. Every resulting segment must be finite, strictly increasing, continuous, and within the inclusive declared slope range. The planner returns one duration-preserving ContactTransformOperationV1::TimeWarp plus exact ContactTransformBindingV1 per member.

The separate pure time_warp_clip_v1 seam consumes one such member plan only after its caller has selected and bound the corresponding loaded clip. It strictly validates track-local clip/track shape; because this seam has no skeleton input, its host must already have bound every track bone index to the selected, validated skeleton. Fixed aggregate name-byte, track, authored/generated key/value, and work caps refuse before candidate allocation; candidate storage has a derived public upper bound from those admitted rows. LINEAR tracks map every authored key and insert interior map-knot samples; STEP tracks map only authored breakpoints. A map knot and an authored key that are the same instant in the emitted binary32 domain — equal, or one representable place apart, since that domain holds nothing between them — are one emitted key: it keeps the authored time and the authored value and carries the control point's own output, because the control point is the definition of the map at that instant. FootCycleClipWarpKnotV1 is the one definition of that question and time_warp_rows_v1 the one answer to which keys a track emits and in what order; the independent output proof consumes those rows for the sequence alone and derives every expected time and value itself, a knot's own times included. Regression tests freeze those rows and independently perturb producer output times for both knot-bearing row kinds and source times for inserted knots. A coalesced row always retains its authored value: its source time is unused metadata after row selection, and perturbing it alone does not invalidate the correct candidate. That holds for a track's own first and last authored key too: the span test is inclusive of one place on either side, so which side of a rounding step the reconstructed instant lands on does not decide whether that key takes its control point's output. Where two authored keys are one place apart, a knot binds to the key it is bit-equal to rather than the key it is merely beside, and both keys are retained. The one exception is a map knot that is the same instant as a source endpoint, 0 or the duration: the map's exact endpoint rows define the output there and a candidate retains those two instants as themselves, so such a knot contributes nothing. Two knots that name one instant refuse: each is compared against both instants of the knot before it, its own resolved source time and the instant it named, because a knot that coalesced into an authored key answers to a time one place from its own. Distinct output instants that collide after binary32 narrowing refuse too. One-key CUBICSPLINE tracks are retained, and multi-key cubic tracks are retained only for bit-exact constant values with zero tangents. The value caps are the shape-derived three-values-per-cubic-key maxima, so malformed N+1 storage refuses at shape or key bounds before it can become a separate valid value-only case.

Neither pure seam transforms contact fragments, derives root measurements, binds the selected asset to the plan, serializes output artifacts, proves reread results, or publishes a generation directory; those remain later #18 transaction seams described below.

Foot-cycle generation V1 (#18)

The public producer is deliberately one collection command with no destination override:

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

The parameterization's output_directory is resolved relative to that document and must name a previously absent directory below an existing safe parent. The command is available in the glTF-only build; .fbx sources remain available only when the fbx feature is enabled. Every admitted source, including .gltf, is emitted as a self-contained GLB.

For N members, the generation has exactly 3N+1 files in parameterization member order:

members/000000/artifact.glb
members/000000/contact-fragment.json
members/000000/evidence.json
...
aggregate-evidence.json

Logical ids and source keys are evidence fields, never filesystem names. The member evidence identity is urn:animsmith:schema:foot-cycle-member-evidence:1; it binds exact manifest and parameterization inputs, source artifact/config/complete closure, output artifact/complete closure, transformed and independently redetected contact identities, canonical operation identity, declared proof policy, and reread proof residuals. The aggregate identity is urn:animsmith:schema:foot-cycle-aggregate-evidence:1; it repeats the control bindings and policy, preserves ordered member paths and identities, and records checked 3N+1, component bytes, exact total, retained GLB bytes, and source/output/combined metric work. See foot-cycle-member-evidence-v1.schema.json and foot-cycle-aggregate-evidence-v1.schema.json.

All GLBs, canonical transformed fragments, member evidence, and aggregate evidence share one 256 MiB retained/staged cap in addition to their stricter component limits. Every candidate, contact transform, independent reread proof, and bounded serialization finishes before the first filesystem write. The publisher flushes the fixed file set beneath one sibling temporary directory and performs one no-replace directory rename. It does not replace an existing generation or claim power-loss, network-filesystem, or abandoned-temp cleanup guarantees.

Success exits 0 and writes the exact published aggregate-evidence.json bytes to stdout only after publication. A broken stdout is diagnosed best effort and cannot undo or relabel the generation. Asset/capability/map/contact/proof refusal exits 1 with one producer-refusal:1 JSON record and no destination. Malformed or stale controls and bindings, unsafe paths or aliases, I/O, serialization, or publication/race failures exit 2 with empty stdout and no published destination.

Contact fragments (#147)

The contact-fragment V1 identity is urn:animsmith:schema:contact-fragment:1. AnimSmith 0.6.0 ships #152's format-neutral strict core reader, bounded canonicalization seam, and strict one-clip producer. generate contact-fragment includes a manifest-selected collection form that reloads the declared source rather than reading collection-output evidence. Transforms and runtime systems remain out of scope.

It is an importable envelope that can be merged into a host's one authoritative measured sidecar. It binds contact facts to the exact primary source bytes and complete versioned dependency closure, plus producer/tool version and an unambiguous clip reference. artifact is the existing primary InputIdentity; dependency_closure_identity is the existing complete DependencyClosureIdentityV1 rather than the full output-v10 closure record. Both serialize as {sha256, bytes}: sha256 makes the algorithm and digest explicit rather than adding separate algorithm and digest fields. Partial or unavailable dependency coverage refuses fragment generation, and a mismatch in either identity makes an existing fragment stale. The complete captured closure's primary_input must equal artifact; producer and consumer validate that relationship against the captured closure. V1 deliberately binds every dependency in the complete modeled closure, including dependencies such as textures that may not affect contacts. This can refuse generation for an unavailable unrelated dependency, but avoids a second format-specific relevance policy.

Collection-owned clips use the #409 logical identity plus its source, take-index, and exact take-name witness. Standalone documents use an exact embedded clip/take name scoped by both input identities; ambiguous or duplicate names are refused. No animation-array index, filename, or engine asset handle is an identity.

Collection-manifest V1 rejects duplicate logical clip ids while parsing as a collection control error, before collection generate-contact-fragment can select a clip. A valid collection take-index addresses the raw source take row; the producer resolves that row's complete normalized-index witness before sampling its internal document clip.

Points and windows use normalized clip time [0, 1], with the measured positive duration recorded for validation. Events have stable opaque ids, engine-neutral roles and phases, and deterministic ordering. V1's closed role vocabulary is left_foot, right_foot, left_hand, right_hand, left_toe, right_toe, left_knee, right_knee, left_elbow, right_elbow, root, prop, and body; its phases are begin, end, and marker. Trim, slice, and time-warp operations must transform the fragment with the same operation or return structured per-event outcomes/refusal. Events outside the retained interval are never silently clamped, dropped, or retained.

Confidence, when present, is finite and lies in [0, 1]. Core fields are closed. Extensions use strict envelopes with exactly schema, schema_version, and payload, where the extension's versioned schema owns the payload object; an unsupported but well-formed extension may be preserved as opaque data. Any transformer must understand and apply every extension's operation contract or refuse the whole operation with unsupported_extension; it may not copy opaque extension-owned times or event references unchanged.

The strict V1 reader caps a contact-fragment source at 8,388,608 bytes and 32 JSON container levels, with at most 4,096 events and 256 extensions. Authored strings are at most 4,096 UTF-8 bytes and identifiers at most 255 bytes; each extension payload is at most 262,144 canonical bytes and 16 levels deep. A complete fragment is at most 8,388,608 canonical bytes. A transform-result source and complete canonical result are each at most 16,777,216 bytes, with at most 4,096 control points and 4,096 event outcomes; its inline fragment obeys the fragment limits. Depth counts every object/array on the root-to-value path, including the root envelope as depth 1. A payload object also starts at depth 1 for its separate 16-level limit. Each exact maximum is accepted; N+1 is rejected during bounded decoding before the excess value is retained or an unbounded canonical buffer is allocated; canonical-byte limits use a bounded JCS sink. JSON Schema maxLength counts Unicode code points, while this reader's text and identifier limits count UTF-8 bytes, so a non-ASCII value may satisfy the schema and still be correctly refused by the reader. The reader accepts decoded IEEE-754 numbers only within the safe magnitude [-9007199254740991, 9007199254740991]; this includes identity byte counts and opaque extension payload numbers. Extension payloads are stored from their bounded canonical JCS bytes, so canonical serialization followed by strict reread preserves the validated fragment value. An explicitly empty extensions array is refused so omitted and present data do not canonicalize to the same value.

A minimal collection-scoped envelope is:

{
  "schema": "urn:animsmith:schema:contact-fragment:1",
  "schema_version": 1,
  "producer": {"tool": "animsmith", "version": "0.6.0"},
  "artifact": {"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "bytes": 123456},
  "dependency_closure_identity": {"sha256": "1111111111111111111111111111111111111111111111111111111111111111", "bytes": 456},
  "clip": {"scope": "collection", "logical_id": "com.example.pack/locomotion/walk-forward-in-place", "source": "walk-forward", "take_index": 0, "take_name": "Take 001"},
  "duration_s": 1.2,
  "events": [{"event_id": "left-foot/0", "role": "left_foot", "phase": "marker", "time": 0.23, "confidence": 0.92}],
  "extensions": [{"schema": "urn:example:contact-quality:1", "schema_version": 1, "payload": {"quality": "high"}}]
}

Seconds and frame values, when shown, are derived display values rather than identity or comparison coordinates. Canonical bytes use the complete RFC 8785 JSON Canonicalization Scheme (JCS), including its object-key, string, and number rules; every extension payload must also be JCS-canonicalizable. Before JCS serialization, events use the exact mixed-event sort tuple (start, kind_rank, end_key, role, phase, event_id): points have kind rank 0 with start = time and a null end sentinel that sorts before every numeric window end; windows use their declared start, have kind rank 1, and use their numeric end. This keeps point/window order stable when starts coincide. Tuple strings, including opaque event ids, compare by unsigned UTF-16 code units exactly like RFC 8785 property names, without Unicode normalization.

Trim, slice, resample, and time-warp use the separate urn:animsmith:schema:contact-transform-result:1 result. It binds input and, on success, output primary, dependency-closure, and canonical-fragment identities. Its strict top-level fields are schema, schema_version, operation, input, outcome, and event_outcomes, with output only on success and refusal { code, message } only on refusal. operation is a strict tagged object: trim/slice use interval { start, end }, resample uses mapping = "identity", and time_warp uses required finite positive output_duration_s plus ordered control_points with input_time/output_time from (0,0) to (1,1). Between adjacent knots (x0, y0) and (x1, y1), it maps t by the exact piecewise-linear formula y0 + ((t - x0) / (x1 - x0)) * (y1 - y0); exact knots map exactly, and both window endpoints use the same rule. A known V1 tag with an invalid numeric domain or ordering remains representable for an invalid_mapping refusal and uses an empty pre-inventory event_outcomes list. An unknown kind, version, field, mapping token, missing field, or malformed field type is a strict request/reader error and produces no V1 result or event outcomes. input has exactly {artifact:{sha256,bytes},dependency_closure_identity:{sha256,bytes},fragment:{sha256,bytes}} and refers to a separately supplied input fragment. Successful output has exactly {artifact:{sha256,bytes},dependency_closure_identity:{sha256,bytes},fragment:{sha256,bytes},contact_fragment}; all three identities must match the inline complete contact-fragment:1 and its two input bindings. The output closure identity is freshly captured rather than copied, and its closure primary_input must equal output artifact. For trim/slice [a,b], a point is outside exactly when t < a || t > b; endpoints are included. A window is outside exactly when end < a || start > b, contained exactly when a <= start && end <= b, and otherwise boundary-crossing. A crossing window receives refused with code partial_window and refuses the whole operation. Known-operation mapping failures, binding mismatches, unsupported extensions, or partial windows produce a typed refused result with no output field. After binding and identity validation, event_outcomes has exactly one {event_id, outcome} object per input event in canonical input order. It adds value only for transformed exact point/window values and code only for refused events; pre-inventory binding, identity, mapping-validation, or extension-support refusal uses an empty list. Global success requires all events to be transformed or outside and requires output; global refusal requires top-level refusal and omits output. The inline output duration is input_duration_s * (b - a) for trim/slice, the input duration for resample, and the required output_duration_s for time_warp; the inline fragment and operation must agree exactly. A rounded trim/slice duration that is not finite and positive refuses before event inventory with invalid_value and an empty event_outcomes list. All time/duration inputs and results are finite IEEE 754 binary64. The normative sequence is dx = rn(x1 - x0), alpha = rn(rn(t - x0) / dx), dy = rn(y1 - y0), and mapped = rn(y0 + rn(alpha * dy)); an exact knot bypasses interpolation. Trim/slice uses span = rn(b - a), mapped = rn(rn(t - a) / span), and output_duration_s = rn(input_duration_s * span). Here rn is IEEE 754 binary64 round-to-nearest, ties-to-even, with no fused or extended-precision intermediate, so independent producers feed the same numeric results to JCS. Refusal codes are partial_window, invalid_mapping, invalid_binding, invalid_value, and unsupported_extension. Malformed fragments/results and duplicate, missing, or unknown event-outcome identities are strict reader errors, not refusal results.

animsmith-core implements this mapping and strict result reader. The reader requires the separately supplied input fragment plus an external transform context containing the current input/output artifacts, their complete captured closures, the expected producer, and any handler-produced extension outputs. It validates each closure against its artifact and independently rederives the operation, event outcomes, and successful inline fragment. Opaque extensions are never copied merely because a caller lists their schema/version: an exact handler-produced output is required at every input extension position. Its published JSON Schema is contact-transform-result-v1.schema.json. Asset time mutation and generation-directory publication are separate collection-producer responsibilities.

The success shape is an illustrative frozen 0.5.0 transform-contract snapshot:

{
  "schema": "urn:animsmith:schema:contact-transform-result:1",
  "schema_version": 1,
  "operation": {"kind": "trim", "version": 1, "interval": {"start": 0.1, "end": 0.9}},
  "input": {"artifact": {"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "bytes": 123456}, "dependency_closure_identity": {"sha256": "1111111111111111111111111111111111111111111111111111111111111111", "bytes": 456}, "fragment": {"sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", "bytes": 789}},
  "outcome": "transformed",
  "event_outcomes": [
    {"event_id": "left-foot/0", "outcome": "transformed", "value": {"time": 0.1625}},
    {"event_id": "right-foot/0", "outcome": "transformed", "value": {"window": {"start": 0.7625, "end": 0.8625}}}
  ],
  "output": {
    "artifact": {"sha256": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210", "bytes": 123456},
    "dependency_closure_identity": {"sha256": "2222222222222222222222222222222222222222222222222222222222222222", "bytes": 654},
    "fragment": {"sha256": "a1d63b5e10381d2aa635cfd74218a4e5d42f3d61c1451765e25d47a23e02d633", "bytes": 729},
    "contact_fragment": {
      "schema": "urn:animsmith:schema:contact-fragment:1",
      "schema_version": 1,
      "producer": {"tool": "animsmith", "version": "0.5.0"},
      "artifact": {"sha256": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210", "bytes": 123456},
      "dependency_closure_identity": {"sha256": "2222222222222222222222222222222222222222222222222222222222222222", "bytes": 654},
      "clip": {"scope": "collection", "logical_id": "com.example.pack/locomotion/walk-forward-in-place", "source": "walk-forward", "take_index": 0, "take_name": "Take 001"},
      "duration_s": 0.96,
      "events": [
        {"event_id": "left-foot/0", "role": "left_foot", "phase": "marker", "time": 0.1625, "confidence": 0.92},
        {"event_id": "right-foot/0", "role": "right_foot", "phase": "begin", "window": {"start": 0.7625, "end": 0.8625}}
      ]
    }
  }
}

The delivered contact-fragment producer samples strict stance-support contacts and writes its own canonical fragment sidecar. Before constructing its metric grid, the producer requires finite positive timing and checked frames * bones pose cells plus frames * tracks sampling work, each at or below the inclusive one-million V1 CLI-producer ceiling. A refusal preserves the destination. These resource bounds are not engine or artistic evidence. The producer does not transform contacts, validate foot placement, map engine-native event types, merge into a host's final sidecar, or establish runtime behavior, gameplay meaning, or engine correctness.

Transition families (#148)

The transition-family V1 identity is urn:animsmith:schema:transition-family:1. A declaration has a stable family id, an explicit ordered member list, a boundary selection (entry, exit, or both), and closed typed tolerances with explicit units and basis.

The document-local transition_families tables are admitted by AnimSmith's strict config loader, which retains the exact whole-config input identity with the normalized declaration. animsmith evaluate-transition-poses INPUT --format json evaluates one document against that authority. The separate collection envelope is evaluated by animsmith collection evaluate-transition-poses COLLECTION.toml --families TRANSITION_FAMILIES.toml --format json: it first binds one exact manifest identity and verifies every logical/source/take witness, then reloads only the source keys selected by the declared members. A stale manifest or member witness is control (exit 2, no result); unavailable member input makes its whole family incomplete rather than comparing a survivor subset.

The format-neutral core provides the strict V1 skeleton-basis identity and immutable transition-pose evaluation result contract at urn:animsmith:schema:transition-pose-evaluation:1. The document and manifest-bound collection adapters publish the JSON-only commands above. The schema is checked in as transition-pose-evaluation-v1.schema.json. That docs schema is canonical; the core crate ships an exact checked snapshot so packaged integration tests retain the same contract without a repository path dependency. Its subject_input field binds the exact raw subject selected by the declaration scope: the document evaluator supplies document bytes and the collection evaluator supplies manifest bytes without forking the V1 result wire. The document evaluator accepts the loader's same-load DependencyClosureV1, derives subject_input only from its primary_input, and retains the complete subject_dependency_closure_identity both at the subject and as each member's source_dependency_closure_identity. A configured family cannot be complete unless closure coverage is complete and that identity is present; otherwise it is incomplete/not_evaluated with dependency_closure_incomplete. Empty document declarations still produce no_configured_families without requiring a closure because no source data is evaluated.

For collection scope, subject_input is the manifest identity and no manifest dependency closure is invented. Each available member instead binds its own raw source_input and complete source_dependency_closure_identity. A source that could not be opened may retain a null source_input; an incomplete member closure leaves its closure identity absent, makes the whole family dependency_closure_incomplete, and does not erase closure identities retained for other available members. This closure binding covers external glTF buffers that can change sampled animation while the primary JSON identity stays equal.

Document-local family ids are one lowercase-ASCII token, 1–255 bytes, starting with [a-z0-9] and continuing with [a-z0-9._-]. The table key itself is the id, with no duplicate family_id field; quote the key whenever punctuation or a dot is present (the canonical spelling quotes it always), such as [transition_families."walk_to_run"] or [transition_families."combat.entry.v1"]. Collection family ids retain Appendix F's slash-qualified logical-id grammar and collection-id prefix.

The declaration distinguishes two ownership scopes:

  • document families are placed in the existing animsmith.toml under [transition_families."<family_id>"] and resolve exact embedded clip/take identities. The reusable config carries no artifact digest; the document evaluator binds the primary document InputIdentity and complete dependency-closure identity in its output.
  • collection families use a separate declaration envelope, not the collection-manifest V1 itself. The envelope binds the exact manifest InputIdentity {sha256, bytes}, then resolves declared logical clip ids plus their source, take-index, and take-name witnesses.

Members cannot cross scopes, point to paths or animation-array indices, or be silently removed when missing or ambiguous. stale_digest is collection-only: it means the declaration's manifest InputIdentity no longer matches. A source digest pin in that manifest is enforced by manifest resolution; a mismatch makes the member unavailable rather than creating a second identity in this declaration. Reusable document-local config has no stale-digest state at parse time; the document evaluator binds the exact primary document InputIdentity plus its complete dependency-closure identity and reports source/take resolution in its output. The existing [clips], [gait_groups], and [sync_groups] sections remain document-local and are not replaced by a second collection authority. Both placements share the transition-family:1 family-record semantics; only ownership and placement differ. Family declarations are canonicalized by stable family id while preserving member order.

Both placements cap the exact declaration and normalized envelope at 8,388,608 source or canonical bytes and 16 container levels. They permit at most 4,096 families, 4,096 members per family, and 16,384 members in aggregate. Authored strings are at most 4,096 UTF-8 bytes, with the 255-byte identifier limits applied more strictly. Depth counts each table/array or JSON object/array on the root-to-value path, including the root as depth 1. Each exact maximum is accepted; N+1 is rejected during bounded decoding before retention or unbounded canonical allocation; canonical-byte limits use a bounded JCS sink.

A collection-scoped declaration uses the following typed TOML-like shape; each logical id is accompanied by the #409 source/take witness and must agree with the manifest:

schema = "urn:animsmith:schema:transition-family:1"
schema_version = 1
scope = "collection"
collection_id = "com.example.pack"
manifest_input_identity = { sha256 = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", bytes = 9876 }

[[families]]
family_id = "com.example.pack/transitions/walk-to-run"
boundary = "both"

[families.basis]
translation = "skeleton-local-metres"
rotation = "skeleton-local-degrees"
time = "normalized-clip"

[families.tolerances]
translation_m = 0.05
rotation_deg = 5.0
time_normalized = 0.02

[[families.members]]
logical_id = "com.example.pack/locomotion/walk-forward-in-place"
source = "walk-forward"
take_index = 0
take_name = "Take 001"

[[families.members]]
logical_id = "com.example.pack/locomotion/run-forward-in-place"
source = "run-forward"
take_index = 0
take_name = "Take 001"

The document-local placement uses the existing config basis and the same family-record semantics:

[transition_families."walk_to_run"]
schema = "urn:animsmith:schema:transition-family:1"
schema_version = 1
scope = "document"
boundary = "entry"

[transition_families."walk_to_run".basis]
translation = "skeleton-local-metres"
rotation = "skeleton-local-degrees"
time = "normalized-clip"

[transition_families."walk_to_run".tolerances]
translation_m = 0.05
rotation_deg = 5.0
time_normalized = 0.02

[[transition_families."walk_to_run".members]]
take_index = 0
take_name = "Walk"

[[transition_families."walk_to_run".members]]
take_index = 1
take_name = "Run"

The collection envelope's manifest digest/bytes binding makes a manifest edit or reorder stale even when collection and logical ids are unchanged. No runtime check or transition graph is implied by either placement.

Both placements normalize to the exact closed JSON envelopes shown in DESIGN.md §F.11: one collection form retaining collection_id and manifest_input_identity, and one document form in which each quoted table key becomes family_id. Families sort by id, member order is retained, and RFC 8785 JCS produces the normalized declaration identity. V1 evidence separately binds the exact declaration source, normalized declaration, and evaluated document or manifest identities.

AnimSmith 0.6.0 ships both the document and manifest-bound collection transition-pose commands from #153. They emit the V1 result but do not infer graph edges, add gameplay metadata, or generate engine state-machine or blend-tree data. Those runtime and gameplay consumers remain follow-up work, including #164.

The complete normative contract, including ownership, strict resolution, and canonical serialization rules, is in DESIGN.md §F.10 and §F.11.

Reference

  • Configuration reference — Complete animsmith.toml tables, keys, defaults, precedence, validation, globs, rig profiles, and engine settings.
  • Built-in check reference — Look up every registered built-in check: current IDs, default findings, prerequisites, config keys, gap semantics, and remediation boundaries.
  • CLI reference — Look up a command, flag, or exit code.
  • Machine-readable output — Parse versioned --format json reports, glTF animation-addressability inventories, engine import advice, and producer evidence in a pipeline, validated by the JSON Schema under schemas/.

animsmith.toml configuration reference

animsmith.toml is an optional, strict project contract. Document-local commands (lint, measure, inspect, report, transform, fix, generate, and evaluate-transition-poses) use --config FILE, or auto-load ./animsmith.toml; no file means built-in defaults. Collection commands have their own control-file rules (see cli.md). Unknown tables and keys are errors. The parser validates finite numeric domains; embedded callers should also call Config::validate.

This page is the user-facing authority for what every key means. For who owns each surface and where its value comes from, start from declaring the contract. Exact Rust types are in the animsmith-core::config API. The check rationale and coverage vocabulary are in game-ready-clips.md.

Precedence and the run model

CLI --select, --allow, and --deny-warnings apply after the file is read; CLI selection and failure policy therefore override file run policy. severity controls content findings, not coverage gaps or required engine prediction-unavailable facets. off disables an applicable check only where that check permits it; note, warn (or warning), and error enable an opt-in check. --allow ID hides matching content findings from the CLI gate; it never repairs input or suppresses required prediction work.

Clip selectors are resolved field-by-field. Every matching clips glob is overlaid in lexicographic key order; a literal clip-name entry is applied last. Thus an exact key wins over every glob, and a later lexicographically greater glob wins only for fields it declares. * matches any sequence (including an empty sequence); there is no ? or character-class syntax. Empty or missing selector lists declare no policy. Group member names are exact (not globs).

Top-level tables

Table/keyType; default; units; allowed valuesOmission and consumerPrerequisite / precedence
[rig]table; { profile = "auto", roles = {}, required_bones = none }Optional. Config::rig feeds role-aware checks, measurements, inspect, assembly, and engine paths.auto scores built-ins; explicit profile or inline roles are resolved by the frontend before checks.
[checks.<id>]map of check id → table; absent settings use each check's built-in defaultsConfig::checks is consumed by the check runner and owning check.--select narrows the catalog; severity is evaluated after selection.
[runtime_nodes]table; absent; selectors is an optional string arrayConfig::runtime_nodes supplies the shared runtime-node policy.If present it wins over legacy checks.rest-world-scale.node_selectors; both is an error.
[clips."selector"]map of exact clip name or * glob → table; all fields omitted by defaultConfig::clips supplies effective expectations to clip-aware checks and transforms.Glob overlay then exact overlay; omission means “inherit/no declaration”, not false or zero.
[gait_groups."name"]named table; no groups by defaultConfig::gait_groups is consumed by gait-group.Needs declared members, resolved feet, and measurable gait evidence; missing work is a coverage gap.
[sync_groups."name"]named table; no groups by defaultConfig::sync_groups is consumed by sync-group.Needs exact member clips and usable timing; optional time_complement adds a phase diagnostic.
[engine]CLI-only table; absent means no engine prediction/adviceEngineToml supplies profile selection and settings to generate addressability, generate import-advice, and engine checks.profile, profile_revision, engine_version, and importer are all required when any selection field is present. Unsupported tuples/settings fail closed.
[transition_families."id"]CLI-only strict document declaration; absent/empty means no familiesevaluate-transition-poses consumes transition_families; the collection variant consumes its --families control file. It is not part of core Config.Exact schema/scope and bounded declaration; see collection-contracts.md.

Rig selection

KeyType/default/allowed valuesOmission, consumer, and prerequisites
rig.profilestring; "auto"; auto, mixamo, ue-mannequin, humanoidauto chooses the unique best built-in match (at least two roles). A named profile is strict. Exact names are tried first, then one unique ASCII-case-insensitive match; ambiguity is typed.
rig.roles.<role>string map; empty; role is root, hips, spine, head, left_foot, right_foot, left_toe, right_toe, left_hand, right_handInline bindings are exact and override the selected profile only for named roles. Missing/colliding bones produce typed coverage/ambiguity, never guesses.
rig.required_bonesstring array or omitted; no units; any bone namesPresence-only structural contract for sockets, IK targets, and masks. It does not require animation keys; use clips.*.animates_bones for that.

Checks and severity

Every check accepts severity = "off", "note", "warn"/"warning", or "error". Omitted severity uses the check's implementation-defined default: all built-ins are enabled by default except opt-in constant-nonunit-scale. time-complement is enabled by default, but is NotApplicable until a sync group declares a time_complement policy; an explicit non-off severity enables the opt-in scale check. The runner still records unselected, disabled, and not-applicable checks. See game-ready-clips.md.

Check settingType; built-in default; units/domainConsumer, activation, and skip behavior
checks.<id>.severityenum; omitted; off, note, warn/warning, errorAll checks. Content findings only; it does not hide gaps or required predictions.
checks.loop-seam.max_ratiofinite non-negative float; 1.5; dimensionlessloop-seam; declared loops with usable foot-cycle evidence.
checks.loop-seam.min_stride_step_mfinite non-negative float; 0.02; metresloop-seam stride floor; short/unresolved gait evidence is a gap.
checks.loop-closure.max_position_delta_mfinite non-negative float; 0.01; metresloop-closure; declared loops; per-clip cap overrides it.
checks.loop-closure.max_rotation_delta_degfinite non-negative float; 1.0; degreesloop-closure; declared loops; per-clip cap overrides it.
checks.loop-seam-vel.max_velocity_delta_mpsfinite non-negative float; 0.1; metres/secondloop-seam-vel; declared loops; per-clip cap overrides it.
checks.loop-seam-rot.max_angular_velocity_delta_degpsfinite non-negative float; 5.0; degrees/secondloop-seam-rot; declared loops; per-clip cap overrides it.
checks.frozen-bone.min_rotation_degfinite non-negative float; 1.0; degreesfrozen-bone; needs animates_bones and a resolvable animated track; missing evidence is a gap.
checks.bind-pose.max_mean_rest_delta_degfinite non-negative float; 45.0; degreesbind-pose; needs rotation tracks and a valid rest pose; otherwise a gap.
checks.foot-slide.contact_height_mfinite non-negative float; 0.03; metresfoot-slide; needs speed_mps, foot roles, and stance evidence.
checks.foot-slide.max_slide_mpsfinite non-negative float; 0.3; metres/secondfoot-slide; same prerequisites as contact_height_m.
checks.rest-world-scale.expected_uniform_scalefinite positive float; 1.0; dimensionless factorrest-world-scale; dormant unless runtime selectors are nonempty; selectors resolve exactly one source node.
checks.rest-world-scale.uniform_scale_tolerancefinite non-negative float; 0.0001; dimensionless factorInclusive tolerance for the expected factor; same selector prerequisite.
checks.rest-world-scale.node_selectorsoptional string array; absent; exact names or * globsLegacy alias for runtime_nodes.selectors; only used when shared field is absent. Both spellings are invalid together.

Settings not consumed by a particular check are harmless in that check's table, but a misspelled field is rejected by deny_unknown_fields.

Clip expectations

KeyType; default; units/allowed valuesConsumer, omission, and prerequisites
clips.<selector>.loopoptional bool; omitted; true/falseDeclares cyclic intent. true activates loop checks; omission is not false evidence for other checks.
clips.<selector>.max_loop_position_delta_moptional finite non-negative float; omitted; metresPer-selector loop-closure cap; inherits global then 0.01.
clips.<selector>.max_loop_rotation_delta_degoptional finite non-negative float; omitted; degreesPer-selector loop-closure cap; inherits global then 1.0.
clips.<selector>.max_loop_velocity_delta_mpsoptional finite non-negative float; omitted; metres/secondPer-selector loop-seam-vel cap; inherits global then 0.1.
clips.<selector>.max_loop_angular_velocity_delta_degpsoptional finite non-negative float; omitted; degrees/secondPer-selector loop-seam-rot cap; inherits global then 5.0.
clips.<selector>.duration_soptional { value: finite positive float, tolerance: finite non-negative float }; omitted; secondsduration-sanity pin. Omission means no duration contract, not zero.
clips.<selector>.speed_mpsoptional pinned value/tolerance; omitted; metres/second; value positive, tolerance non-negativeRoot-motion speed and foot-slide; the latter is not applicable without this declaration.
clips.<selector>.movement_owner_xz, clips.<selector>.movement_owner_y, clips.<selector>.movement_owner_yawoptional enum; omitted; gameplay or animationIn-place/root-motion checks and downstream transform intent. Each axis is independent; omission is never inferred.
clips.<selector>.in_placeoptional bool; omitted; true = gameplay XZ, false = animation XZLegacy XZ alias for clips.<selector>.movement_owner_xz. Cannot coexist with it in one entry; across layers it is normalized before overlay.
clips.<selector>.fpsoptional finite positive float; omitted; frames/secondfps and sync-group; omission means no authored frame-grid contract.
clips.<selector>.animates_bonesoptional string array; omitted; bone namesmissing-bones requires keys and frozen-bone requires actual rotation movement. Distinct from rig.required_bones.

Runtime nodes and globs

runtime_nodes.selectors is an ordered string array. Each item is an exact source-node name or a * glob and must resolve to exactly one named node for rest-world-scale; no match and multiple matches are typed coverage gaps. Duplicate spellings are de-duplicated at first occurrence. Selectors do not cross node and bone namespaces, and do not infer a node from a rig role.

Gait and same-time groups

KeyType/default/domainConsumer and skip behavior
gait_groups.<name>.clipsstring array; required; exact clip namesgait-group checks member existence, then phase for measurable members.
gait_groups.<name>.max_gait_phase_spreadfinite float [0, 0.5]; required; cycle fractionMaximum circular phase spread; missing roles/short clips/low evidence produce coverage.
gait_groups.<name>.min_lr_amplitude_mfinite non-negative float; 0.0; metresExcludes low-amplitude members as noise.
sync_groups.<name>.clipsstring array; required; exact clip namessync-group checks exact members at the same absolute time.
sync_groups.<name>.max_duration_delta_sfinite non-negative float; required; secondsLargest permitted duration range.
sync_groups.<name>.max_frame_count_deltaunsigned integer; required; key-count differenceLargest permitted longest-channel key-count range.
sync_groups.<name>.max_fps_deltafinite non-negative float; required; frames/secondLargest permitted declared-FPS range.
sync_groups.<name>.time_complementoptional table; omitted disables diagnosticAdds reflected-time phase comparison for each configured-order unordered pair.
sync_groups.<name>.time_complement.min_reflected_time_advantagefinite float [0, 1]; required; scoreMinimum reflected-minus-same phase similarity advantage.
sync_groups.<name>.time_complement.min_lr_amplitude_mfinite non-negative float; required; metresMinimum evidence amplitude before phase comparison.

Engine profiles (CLI)

The four selection keys have no implicit default: engine.profile, engine.profile_revision, engine.engine_version, and engine.importer. Either omit [engine] or specify all four. Exact supported tuples and setting defaults are owned by the versioned animsmith-engine profiles. The profile is evidence about importer behavior, not a claim that an engine loaded or played the asset.

engine.settings accepts profile-defined keys; unknown keys or values are rejected when the selected profile resolves. Common revision-1 settings are engine.settings.convert_units (bool), engine.settings.bake_axis_conversion (bool), and engine.settings.root_motion_source (string path), plus per-clip clips.<selector>.engine_settings.root_rotation, clips.<selector>.engine_settings.root_position_y, and clips.<selector>.engine_settings.root_position_xz (bake or extract). Revision-2 profiles may define engine.settings.animation_type (generic|humanoid|legacy), engine.settings.avatar_setup (create_from_this_model|copy_from_other_avatar), engine.settings.import_animation (bool), engine.settings.root_motion_source (string), engine.settings.root_rotation, engine.settings.root_position_y, engine.settings.root_position_xz (bake|extract), engine.settings.rotate_scene_entity (bool), engine.settings.rotate_meshes (bool), engine.settings.load_meshes (empty|nonempty), engine.settings.extension_handler_environment (bare_empty|bevy_pbr_stock_0_19), engine.settings.bevy_animation_feature (bool), engine.settings.load_animations (bool), engine.settings.animation_fps (positive integer), engine.settings.animation_trimming (bool), and engine.settings.sample_rate (default_30, source_determined, or custom_hz(N)). Per-clip revision-2 root policies use the same clips.<selector>.engine_settings.root_rotation, clips.<selector>.engine_settings.root_position_y, and clips.<selector>.engine_settings.root_position_xz paths. A setting is either explicit or the profile's verified default; no generic default is invented.

Transition families

Document-local families are strict tables, not core Config fields:

PathAuthority and contract
transition_families.<id>DocumentConfigWire::transition_families map entry; the family id is the exact TOML key.
transition_families.<id>.schemaDocumentFamilyWire::schema; must be the supported transition-family schema id.
transition_families.<id>.schema_versionDocumentFamilyWire::schema_version; currently 1.
transition_families.<id>.scopeDocumentFamilyWire::scope; currently document.
transition_families.<id>.boundaryDocumentFamilyWire::boundary; entry, exit, or both.
transition_families.<id>.basisBasisWire; all three basis fields are required.
transition_families.<id>.basis.translationBasisWire::translation; currently skeleton-local-metres.
transition_families.<id>.basis.rotationBasisWire::rotation; currently skeleton-local-degrees.
transition_families.<id>.basis.timeBasisWire::time; currently normalized-clip.
transition_families.<id>.tolerancesTolerancesWire; all three finite non-negative limits are required.
transition_families.<id>.tolerances.translation_mTolerancesWire::translation_m, in metres.
transition_families.<id>.tolerances.rotation_degTolerancesWire::rotation_deg, in degrees.
transition_families.<id>.tolerances.time_normalizedTolerancesWire::time_normalized, in normalized clip time.
transition_families.<id>.membersDocumentFamilyWire::members; bounded ordered member list.
transition_families.<id>.members.take_indexDocumentMemberWire::take_index; zero-based take index.
transition_families.<id>.members.take_nameDocumentMemberWire::take_name; exact source take name.
[transition_families."walk_to_run"]
schema = "urn:animsmith:schema:transition-family:1"
schema_version = 1
scope = "document"
boundary = "both"                 # entry | exit | both
[transition_families."walk_to_run".basis]
translation = "skeleton-local-metres"
rotation = "skeleton-local-degrees"
time = "normalized-clip"
[transition_families."walk_to_run".tolerances]
translation_m = 0.05
rotation_deg = 5.0
time_normalized = 0.05
[[transition_families."walk_to_run".members]]
take_index = 0
take_name = "walk"

The complete bounded grammar and allowed values are documented in collection-contracts.md. It is read before generic config decoding, so malformed declarations report a transition-family control error and never become ordinary check findings.

Minimal and advanced configurations

The smallest semantic contract is one loop declaration:

[clips.walk]
loop = true

For a project contract, start with examples/character.animsmith.toml. It demonstrates rig roles, runtime-node globs, independent movement ownership, glob/exact cap precedence, severity, gait rings, and same-time groups. The Mixamo tutorial and examples/mixamo.animsmith.toml show the profile with an in-place XZ declaration. Bevy examples are bevy.animsmith.toml, bevy-v2.animsmith.toml, and bevy-v3.animsmith.toml. CI parse-tests these examples and this page's key inventory; update that test when adding configuration authority.

Common mistakes and errors

  • unknown field or unknown variant: spelling/casing is not supported.
  • must be a finite non-negative number: a cap, tolerance, or amplitude is negative, NaN, or infinite. Positive-only values have the corresponding positive error.
  • cannot declare both ...: choose one runtime selector spelling and one horizontal movement-owner spelling per entry.
  • bad config ...: parser, validation, profile, and transition-family errors include the selected path and exit 2; no asset is linted. A coverage gap during a valid run is evidence of a missing prerequisite, not a parse error.

When changing a contract, run with --format json and retain the config path and tool version with the output. A clean result is evidence for that file and declaration set, not certification of retargeting, engine runtime behavior, or artistic quality.

Built-in check reference

This is the customer-facing reference for every registered built-in check in AnimSmith's current catalog. It complements the concise README check catalog, what game-ready means, and the exact Rust API on docs.rs.

Use this page when you need the current built-in ID, default findings, declarations or prerequisites, config keys and units, skip or coverage-gap semantics, and the practical boundary between built-in tooling, runtime policy, and DCC-side correction.

off in the inventory below means the check is registered but opt-in by default. Once selected, every built-in check can still be made inactive with [checks.<id>] severity = "off", and any selected opt-in check becomes active only after an explicit note, warn, or error severity.

Inventory

idclassdefault findingsdeclarations or prerequisitesconfig keystooling
nanmechanicalerrornoneseverityDCC/export repair
time-monotonicmechanicalerror, notenoneseverityDCC/export repair
quat-normmechanicalerrornoneseverityfix
quat-flipmechanicalwarningnoneseverityfix
duration-sanitymechanicalerror, warningnoneseverity, clips.<name>.duration_s.value, clips.<name>.duration_s.toleranceDCC/export repair, transform --slice, transform --hold-extend
scale-keysmechanicalwarningnoneseverityDCC cleanup
non-uniform-scalemechanicalwarningnoneseverityDCC cleanup
constant-nonunit-scalemechanicaloff, notenoneseverityDCC cleanup
constant-trackmechanicalnotenoneseveritytransform --prune-constant-tracks
required-bonescontract-awareerrorrig.required_bones, usable skeletonseverity, rig.required_bonesDCC/export repair
rest-world-scalecontract-awarewarningruntime_nodes.selectors or legacy node selector alias, source-node transform evidenceseverity, runtime_nodes.selectors, checks.rest-world-scale.node_selectors, expected_uniform_scale, uniform_scale_tolerancescale, DCC/export repair
missing-bonescontract-awareerrorclips.<name>.animates_bonesseverity, clips.<name>.animates_bonesDCC/export repair
frozen-bonecontract-awareerrorclips.<name>.animates_bones, keyed bone tracksseverity, min_rotation_deg, clips.<name>.animates_bonesDCC/export repair
duplicate-loop-endpointcontract-awarewarningclips.<name>.loop, authored endpoint analysisseverity, clips.<name>.looptransform --drop-duplicate-loop-endpoint
loop-closurecontract-awareerrorclips.<name>.loop, usable loop-continuity samplesseverity, max_position_delta_m, max_rotation_delta_deg, clips.<name>.loop, clips.<name>.max_loop_position_delta_m, clips.<name>.max_loop_rotation_delta_degDCC/export repair
loop-seamcontract-awareerrorclips.<name>.loop, hips and foot roles, sampled stride stepseverity, max_ratio, min_stride_step_m, clips.<name>.loopDCC/export repair
loop-seam-velcontract-awareerrorclips.<name>.loop, usable loop-continuity samplesseverity, max_velocity_delta_mps, clips.<name>.loop, clips.<name>.max_loop_velocity_delta_mpsDCC/export repair
loop-seam-rotcontract-awareerrorclips.<name>.loop, usable loop-continuity samplesseverity, max_angular_velocity_delta_degps, clips.<name>.loop, clips.<name>.max_loop_angular_velocity_delta_degpsDCC/export repair
root-motion-speedcontract-awareerrorclips.<name>.speed_mps, non-gameplay XZ owner, root/hips role, sampled root travelseverity, clips.<name>.speed_mps.value, clips.<name>.speed_mps.tolerance, clips.<name>.movement_owner_xz, clips.<name>.in_placeDCC/export repair
gait-groupcontract-awareerrorgait_groups.<name>.clips, hips and foot roles, measurable left/right gait amplitudeseverity, gait_groups.<name>.clips, gait_groups.<name>.max_gait_phase_spread, gait_groups.<name>.min_lr_amplitude_mDCC/export repair
sync-groupcontract-awareerrorsync_groups.<name>.clips, at least two present membersseverity, sync_groups.<name>.clips, sync_groups.<name>.max_duration_delta_s, sync_groups.<name>.max_frame_count_delta, sync_groups.<name>.max_fps_deltaDCC/export repair
time-complementcontract-awarewarningsync_groups.<name>.time_complement, hips and foot roles, measurable same-time gait phaseseverity, sync_groups.<name>.clips, sync_groups.<name>.time_complement.min_reflected_time_advantage, sync_groups.<name>.time_complement.min_lr_amplitude_mauthor/runtime review
in-placecontract-awareerrorclips.<name>.movement_owner_xz or clips.<name>.in_place, root/hips role, sampled root travelseverity, clips.<name>.movement_owner_xz, clips.<name>.in_placeDCC/export repair
fpscontract-awarewarningclips.<name>.fpsseverity, clips.<name>.fpsDCC/export repair, transform --slice
bind-posecontract-awarewarningclips with at least three usable first-frame rotation tracksseverity, max_mean_rest_delta_degDCC/export repair
foot-slidecontract-awarewarningclips.<name>.speed_mps, root/hips role, resolved foot or toe roles, sampled stance windowsseverity, contact_height_m, max_slide_mpsDCC/export repair, engine review

Mechanical checks

nan

  • Default findings: error.
  • Measurement and finding: flags any non-finite key time or keyed value. A finding means interpolation and pose evaluation are no longer trustworthy.
  • Prerequisites and applicability: none. This is generic file-ready coverage.
  • Config, defaults, and units: [checks.nan] severity only.
  • Inactive, not-applicable, and coverage gaps: never reports not_applicable or a typed coverage gap once selected.
  • Remediation and boundary: there is no safe built-in repair because the source float is undefined. Re-export or correct the source in the DCC/export step.
  • Runtime and references: one poisoned float can explode, freeze, or otherwise corrupt the pose in-engine. See The pose flickers, spins, or explodes and API: Nan.

time-monotonic

  • Default findings: error for negative or non-increasing key times, note for a first key that starts materially after zero.
  • Measurement and finding: checks that key times start at or near zero and move strictly forward. A finding means the runtime may clamp-hold or sample incorrectly the track.
  • Prerequisites and applicability: none. This is generic file-ready coverage.
  • Config, defaults, and units: [checks.time-monotonic] severity only.
  • Inactive, not-applicable, and coverage gaps: never not_applicable and no typed gaps once selected.
  • Remediation and boundary: fix the export or source timing in the DCC. Use transform --slice only for an intentional clip trim, not to salvage invalid key ordering.
  • Runtime and references: late starts can hold an unauthored pose; bad ordering is a hard data defect. See The pose flickers, spins, or explodes and API: TimeMonotonic.

quat-norm

  • Default findings: error.
  • Measurement and finding: reports the worst rotation key whose quaternion norm deviates materially from 1.0.
  • Prerequisites and applicability: none. This is generic file-ready coverage.
  • Config, defaults, and units: [checks.quat-norm] severity only.
  • Inactive, not-applicable, and coverage gaps: never not_applicable and no typed gaps once selected.
  • Remediation and boundary: animsmith fix can renormalize finite, non-zero quaternions losslessly. If the source exporter keeps reintroducing the issue, correct that upstream.
  • Runtime and references: non-unit rotations skew interpolation, skinning, and blends. See The pose flickers, spins, or explodes, CLI repairs, and API: QuatNorm.

quat-flip

  • Default findings: warning.
  • Measurement and finding: counts adjacent rotation keys on opposite hemispheres. A finding means neighborhood-uncorrected slerp can take the long path.
  • Prerequisites and applicability: none. This is generic file-ready coverage.
  • Config, defaults, and units: [checks.quat-flip] severity only.
  • Inactive, not-applicable, and coverage gaps: never not_applicable and no typed gaps once selected.
  • Remediation and boundary: animsmith fix can flip equivalent quaternion signs into a hemisphere-consistent track. Re-export if the source keeps emitting unstable signs.
  • Runtime and references: the visible symptom is a sudden long-way spin between keys. See The pose flickers, spins, or explodes, CLI repairs, and API: QuatFlip.

duration-sanity

  • Default findings: error for invalid or missed declared duration pins and degenerate clip lengths, warning for trackless clips or mismatched channel end times.
  • Measurement and finding: checks clip duration, optional declared duration_s, and spread between multi-key channel end times.
  • Prerequisites and applicability: none. Declared duration pins refine a check that is otherwise generic.
  • Config, defaults, and units: [checks.duration-sanity] severity; optional clips.<name>.duration_s.value and clips.<name>.duration_s.tolerance, both in seconds.
  • Inactive, not-applicable, and coverage gaps: never not_applicable and no typed gaps once selected.
  • Remediation and boundary: correct the authored range or export in the DCC. transform --slice and transform --hold-extend are explicit editing tools when the clip is intentionally being reshaped, not automatic correctness repairs.
  • Runtime and references: bad duration contracts freeze shorter channels or desync gameplay timing. See The clip is the wrong length or freezes at the end and API: DurationSanity.

scale-keys

  • Default findings: warning.
  • Measurement and finding: reports temporally varying scale tracks.
  • Prerequisites and applicability: none. This is generic file-ready coverage.
  • Config, defaults, and units: [checks.scale-keys] severity only.
  • Inactive, not-applicable, and coverage gaps: never not_applicable and no typed gaps once selected.
  • Remediation and boundary: remove accidental animated scale in the DCC or export settings. Treat retained scale animation as an intentional authored choice that downstream runtime and retargeter owners must explicitly accept.
  • Runtime and references: many rigs and retargeters mishandle animated scale. See The file is bloated, or the retargeter chokes and API: ScaleKeys.

non-uniform-scale

  • Default findings: warning.
  • Measurement and finding: reports scale tracks whose evaluated axis lengths are materially unequal.
  • Prerequisites and applicability: none. This is generic file-ready coverage.
  • Config, defaults, and units: [checks.non-uniform-scale] severity only.
  • Inactive, not-applicable, and coverage gaps: never not_applicable and no typed gaps once selected.
  • Remediation and boundary: normalize the authored transform in the DCC or export path unless your runtime and retargeter contract explicitly accepts the non-uniform scale.
  • Runtime and references: non-uniform scale is a common retargeting and rig failure source. See The file is bloated, or the retargeter chokes and API: NonUniformScale.

constant-nonunit-scale

  • Default findings: off by default; once enabled, findings are note.
  • Measurement and finding: reports scale channels that stay constant but not at unit scale, including single-key pins.
  • Prerequisites and applicability: none. This is a policy signal, not a mandatory file-ready failure.
  • Config, defaults, and units: [checks.constant-nonunit-scale] severity only.
  • Inactive, not-applicable, and coverage gaps: inactive until an explicit severity enables it; never reports typed gaps once active.
  • Remediation and boundary: clean up the authored scale in the DCC if your runtime policy expects unit scale. Keep it enabled only when your team wants to surface that policy in lint results.
  • Runtime and references: constant non-unit scale can still affect import, attachment, and retargeting expectations. See The file is bloated, or the retargeter chokes and API: ConstantNonunitScale.

constant-track

  • Default findings: note.
  • Measurement and finding: reports multi-key translation, rotation, or scale tracks that never materially move.
  • Prerequisites and applicability: none. This is generic file-ready coverage.
  • Config, defaults, and units: [checks.constant-track] severity only.
  • Inactive, not-applicable, and coverage gaps: never not_applicable and no typed gaps once selected.
  • Remediation and boundary: transform --prune-constant-tracks can remove the subset AnimSmith can prove redundant. Use DCC cleanup when the track carries authoring intent, unsupported curves, or broader rig cleanup.
  • Runtime and references: constant multi-key tracks are usually export bloat, but removing them still needs contract review for transition and required motion evidence. See The file is bloated, or the retargeter chokes, Editing a clip, and API: ConstantTrack.

Contract-aware checks

required-bones

  • Default findings: error.
  • Measurement and finding: checks that every distinct name in rig.required_bones exists exactly once in the skeleton.
  • Prerequisites and applicability: active only when rig.required_bones is non-empty. A usable skeleton is required.
  • Config, defaults, and units: [checks.required-bones] severity and rig.required_bones.
  • Inactive, not-applicable, and coverage gaps: not_applicable with no declared required bones; reports a skeleton_unavailable coverage gap when the file has no usable skeleton.
  • Remediation and boundary: fix the exported skeleton, naming, or rig mapping in the DCC/export pipeline. This is not a clip-level animation repair.
  • Runtime and references: missing sockets, IK targets, or mask bones break downstream binding even if no clip animates them. See A limb is T-posed, or a bone never moves and API: RequiredBones.

rest-world-scale

  • Default findings: warning.
  • Measurement and finding: checks the effective rest-world linear transform of explicitly selected source nodes against a uniform scale policy.
  • Prerequisites and applicability: active only when runtime_nodes.selectors or the legacy checks.rest-world-scale.node_selectors alias declares at least one selector. Source-node transform evidence must exist, and each selector must resolve exactly once.
  • Config, defaults, and units: [checks.rest-world-scale] severity, expected_uniform_scale default 1.0, and uniform_scale_tolerance default 0.0001; selectors live in runtime_nodes.selectors, with the legacy alias accepted only when the shared field is absent.
  • Inactive, not-applicable, and coverage gaps: not_applicable without selectors; gaps report no-match, ambiguous selector resolution, or unavailable source-node transform evidence.
  • Remediation and boundary: if the source is uniformly wrong, decide between scale whole-document, scale rest-bind, or DCC/export correction based on the actual scale problem. This check reports source-node evidence; it does not infer which rewrite you intend.
  • Runtime and references: wrong rest-world scale breaks sockets, attachments, gameplay references, and engine-side assumptions even when animation keys are otherwise valid. See Scaling glTF safely, glTF and generic runtime guide, and API: RestWorldScale.

missing-bones

  • Default findings: error.
  • Measurement and finding: checks that each declared animates_bones member exists and carries at least one keyed track in the clip.
  • Prerequisites and applicability: active only on clips that declare animates_bones.
  • Config, defaults, and units: [checks.missing-bones] severity and clips.<name>.animates_bones.
  • Inactive, not-applicable, and coverage gaps: not_applicable on clips without animates_bones; no typed coverage gaps once the declaration exists.
  • Remediation and boundary: restore the missing keyed channel or correct the source clip and rig in the DCC/export path.
  • Runtime and references: declared motion that never reaches the file reads as a static or wrong-rig limb at runtime. See A limb is T-posed, or a bone never moves and API: MissingBones.

frozen-bone

  • Default findings: error.
  • Measurement and finding: checks whether each declared animated bone rotates beyond the configured floor across the clip.
  • Prerequisites and applicability: active only on clips that declare animates_bones. The bone must exist and carry at least one keyed track.
  • Config, defaults, and units: [checks.frozen-bone] severity, min_rotation_deg default 1.0 degrees, and clips.<name>.animates_bones.
  • Inactive, not-applicable, and coverage gaps: not_applicable on clips without animates_bones; no typed gaps once the declaration exists. Missing bones themselves belong to missing-bones.
  • Remediation and boundary: correct the source clip, wrong slice, or masked-out track in the DCC/export pipeline.
  • Runtime and references: the visible symptom is a limb that stays pinned, T-posed, or otherwise fails to animate even though the clip says it should. See A limb is T-posed, or a bone never moves and API: FrozenBone.

duplicate-loop-endpoint

  • Default findings: warning.
  • Measurement and finding: checks for the strict authored-key case where a declared loop repeats its first pose at the final endpoint and that closing key can be removed mechanically.
  • Prerequisites and applicability: active only on clips declared loop = true, and only when authored endpoint analysis is available.
  • Config, defaults, and units: [checks.duplicate-loop-endpoint] severity and clips.<name>.loop.
  • Inactive, not-applicable, and coverage gaps: not_applicable when no clip declares a loop. Reports a measurement-unavailable gap when authored endpoint analysis cannot classify the clip.
  • Remediation and boundary: transform --drop-duplicate-loop-endpoint can trim the mechanically removable endpoint subset. Non-classified loop endpoint modes still need authored review or a different transform.
  • Runtime and references: redundant inclusive endpoints are a common DCC export artifact and can affect downstream loop handling. See The loop pops, Editing a clip, and API: DuplicateLoopEndpoint.

loop-closure

  • Default findings: error.
  • Measurement and finding: checks the largest per-bone model-space position and rotation delta between the final and first loop sample.
  • Prerequisites and applicability: active only on clips declared loop = true and only when loop-continuity samples exist.
  • Config, defaults, and units: [checks.loop-closure] severity, max_position_delta_m default 0.01 metres, max_rotation_delta_deg default 1.0 degrees, plus per-clip overrides clips.<name>.max_loop_position_delta_m and clips.<name>.max_loop_rotation_delta_deg.
  • Inactive, not-applicable, and coverage gaps: not_applicable when no clip declares a loop. Reports measurement-unavailable gaps when the sampled loop-continuity evidence is missing or unusable for one or more bones.
  • Remediation and boundary: re-author, reslice, or re-export the loop in the DCC. This is a diagnosis, not an automatic loop repair.
  • Runtime and references: a failed closure produces an obvious pose jump at the wrap. See The loop pops and API: LoopClosure.

loop-seam

  • Default findings: error.
  • Measurement and finding: checks the feet-relative-to-hips wrap discontinuity, normalized by seam-adjacent in-clip step length.
  • Prerequisites and applicability: active only on clips declared loop = true and only when hips plus at least one foot role resolve and a usable stride step can be sampled.
  • Config, defaults, and units: [checks.loop-seam] severity, max_ratio default 1.5, and min_stride_step_m defaulting to the built-in minimum stride floor used by the metrics.
  • Inactive, not-applicable, and coverage gaps: not_applicable when no clip declares a loop. Gaps report unresolved gait roles, clips too short to sample a cycle, unavailable foot-cycle metrics, or no usable stride step.
  • Remediation and boundary: fix the loop cut or cycle in the DCC. AnimSmith intentionally reports the seam evidence rather than inventing a repair.
  • Runtime and references: the symptom is a once-per-cycle pop even when the raw file is format-valid. See The loop pops and API: LoopSeam.

loop-seam-vel

  • Default findings: error.
  • Measurement and finding: checks the largest per-bone model-space linear velocity change between the incoming and outgoing loop seam.
  • Prerequisites and applicability: active only on clips declared loop = true and only when loop-continuity samples exist.
  • Config, defaults, and units: [checks.loop-seam-vel] severity, max_velocity_delta_mps default 0.1 metres per second, plus the per-clip override clips.<name>.max_loop_velocity_delta_mps.
  • Inactive, not-applicable, and coverage gaps: not_applicable when no clip declares a loop. Reports measurement-unavailable gaps when loop-continuity samples are missing or unusable.
  • Remediation and boundary: correct the authored wrap or the export timing in the DCC; there is no built-in automatic velocity repair.
  • Runtime and references: this is the once-per-cycle hitch where the clip hits the right pose but changes speed abruptly at the seam. See The loop pops and API: LoopSeamVelocity.

loop-seam-rot

  • Default findings: error.
  • Measurement and finding: checks the largest per-bone model-space angular velocity change between the incoming and outgoing loop seam.
  • Prerequisites and applicability: active only on clips declared loop = true and only when loop-continuity samples exist.
  • Config, defaults, and units: [checks.loop-seam-rot] severity, max_angular_velocity_delta_degps default 5.0 degrees per second, plus the per-clip override clips.<name>.max_loop_angular_velocity_delta_degps.
  • Inactive, not-applicable, and coverage gaps: not_applicable when no clip declares a loop. Reports measurement-unavailable gaps when loop-continuity samples are missing or unusable.
  • Remediation and boundary: correct the authored wrap or export timing in the DCC; there is no built-in automatic angular-velocity repair.
  • Runtime and references: this is the rotational version of a loop hitch or pulse at the seam. See The loop pops and API: LoopSeamRotation.

root-motion-speed

  • Default findings: error.
  • Measurement and finding: compares measured horizontal root travel against a declared speed_mps pin for clips whose XZ owner is not gameplay.
  • Prerequisites and applicability: active only when a clip declares speed_mps and does not declare gameplay-owned XZ travel. Root or hips role resolution and measurable root travel are required.
  • Config, defaults, and units: [checks.root-motion-speed] severity, clips.<name>.speed_mps.value, clips.<name>.speed_mps.tolerance, and the XZ owner via clips.<name>.movement_owner_xz or the compatibility alias clips.<name>.in_place.
  • Inactive, not-applicable, and coverage gaps: not_applicable with no relevant speed pin. Gaps report unresolved root or hips roles or unavailable measured root-motion speed.
  • Remediation and boundary: fix the clip's actual travel or the declared speed contract in the DCC/export path or config. This is not a runtime retiming suggestion from AnimSmith.
  • Runtime and references: stale speed pins make motion-scaled playback slide or moonwalk. See The character glides or runs in place and API: RootMotionSpeed.

gait-group

  • Default findings: error.
  • Measurement and finding: compares measured gait phases across each declared blend-ring group and reports spread beyond the configured cap.
  • Prerequisites and applicability: active only when gait_groups is non-empty. Hips and foot roles must resolve, and at least two members need measurable gait evidence above the configured amplitude floor.
  • Config, defaults, and units: [checks.gait-group] severity, plus gait_groups.<name>.clips, gait_groups.<name>.max_gait_phase_spread in cycle fraction, and gait_groups.<name>.min_lr_amplitude_m in metres.
  • Inactive, not-applicable, and coverage gaps: not_applicable with no gait groups. Gaps report unresolved gait roles, missing members, too few measurable members, or members excluded because phase evidence is unavailable or below the amplitude floor.
  • Remediation and boundary: correct clip timing, loop structure, or selection in the DCC and review the declared group. AnimSmith does not reorder or retime the group automatically.
  • Runtime and references: phase drift between directional blends shows up as sliding or popping feet when clips crossfade. See Directional blend members travel at different speeds and API: GaitGroup.

sync-group

  • Default findings: error.
  • Measurement and finding: compares declared same-time group members for duration, frame count, frame grid, and loop endpoint mode compatibility.
  • Prerequisites and applicability: active only when sync_groups is non-empty, and each group needs at least two present members before full comparison is possible.
  • Config, defaults, and units: [checks.sync-group] severity, plus sync_groups.<name>.clips, sync_groups.<name>.max_duration_delta_s in seconds, sync_groups.<name>.max_frame_count_delta in frames, and sync_groups.<name>.max_fps_delta in frames per second.
  • Inactive, not-applicable, and coverage gaps: not_applicable with no sync groups. Gaps report missing members, too few present or measurable members, unavailable durations, unusable frame-grid evidence, or unavailable loop endpoint evidence.
  • Remediation and boundary: realign or rebuild the compared clips and the group declaration in the DCC/export pipeline. This check reports compatibility; it does not choose a runtime retiming strategy.
  • Runtime and references: mismatched same-time clips blend or switch at different semantic moments. See Feet skate when clips blend and API: SyncGroupCheck.

time-complement

  • Default findings: warning.
  • Measurement and finding: compares each declared same-time pair's gait-phase similarity against the similarity after reflecting one member's normalized cycle time.
  • Prerequisites and applicability: active only when a sync group enables time_complement. Hips and foot roles must resolve, and at least two members need measurable gait phase above the configured amplitude floor.
  • Config, defaults, and units: [checks.time-complement] severity, plus sync_groups.<name>.time_complement.min_reflected_time_advantage in [0, 1] and sync_groups.<name>.time_complement.min_lr_amplitude_m in metres. The check also uses sync_groups.<name>.clips.
  • Inactive, not-applicable, and coverage gaps: not_applicable with no enabled time-complement policy. Gaps report missing members, unresolved gait roles, unavailable phase evidence, or too few measurable members.
  • Remediation and boundary: treat this as a sync-diagnostic for authoring or runtime review, not as proof that time reflection is the right runtime fix.
  • Runtime and references: a warning means same-time pairing may be working against the measured gait evidence. See Feet skate when clips blend and API: TimeComplement.

in-place

  • Default findings: error.
  • Measurement and finding: compares the declared horizontal movement owner to measured horizontal root travel.
  • Prerequisites and applicability: active only when a clip declares movement_owner_xz or its compatibility alias in_place. Root or hips role resolution and measurable root travel are required.
  • Config, defaults, and units: [checks.in-place] severity, clips.<name>.movement_owner_xz, or the compatibility alias clips.<name>.in_place.
  • Inactive, not-applicable, and coverage gaps: not_applicable when no clip declares horizontal movement ownership. Gaps report unresolved root or hips roles or unavailable measured root speed.
  • Remediation and boundary: fix the clip or the declared ownership in the DCC, export path, or contract config. This is an intent mismatch, not a built-in transform target.
  • Runtime and references: a mismatch becomes visible gliding or running in place. See The character glides or runs in place and API: InPlace.

fps

  • Default findings: warning.
  • Measurement and finding: checks that a declared clip duration spans a whole number of frames and that keyed times stay on the declared frame grid.
  • Prerequisites and applicability: active only on clips that declare fps.
  • Config, defaults, and units: [checks.fps] severity and clips.<name>.fps in frames per second.
  • Inactive, not-applicable, and coverage gaps: not_applicable on clips without fps. Reports an invalid_declared_fps coverage gap when the declared frame rate is non-finite or non-positive.
  • Remediation and boundary: correct the authored frame rate or retiming in the DCC/export path. transform --slice is appropriate for an intentional frame-aligned cut, not for arbitrary resampling drift.
  • Runtime and references: off-grid keys and fractional frame counts are common importer pain points. See The clip is the wrong length or freezes at the end and API: Fps.

bind-pose

  • Default findings: warning.
  • Measurement and finding: checks the mean first-frame rotation deviation from rest across bones with usable rotation tracks.
  • Prerequisites and applicability: active whenever the file has clips. A clip needs at least three usable first-frame rotation tracks before the check can evaluate it.
  • Config, defaults, and units: [checks.bind-pose] severity and max_mean_rest_delta_deg default 45 degrees.
  • Inactive, not-applicable, and coverage gaps: not_applicable only when the document has no clips. Reports insufficient_rotation_evidence when a clip lacks enough usable first-frame rotation tracks.
  • Remediation and boundary: correct the seed rig, export skeleton, or clip source in the DCC/export path. This is not a rest-bind scale rewrite.
  • Runtime and references: a mismatched bind often presents as the wrong base pose or wrong-skeleton clip. See Files disagree about skeleton or clip identity and API: BindPose.

foot-slide

  • Default findings: warning.
  • Measurement and finding: checks whether each stance foot moves consistently with the declared speed_mps and the clip's measured horizontal root speed.
  • Prerequisites and applicability: active only on clips that declare speed_mps. Root or hips roles must resolve, the clip must be long enough to sample stance, and a foot or toe role must resolve for each judged side.
  • Config, defaults, and units: [checks.foot-slide] severity, contact_height_m default 0.03 metres, max_slide_mps default 0.3 metres per second. The clip-level speed_mps declaration is the prerequisite, not a foot-slide check setting.
  • Inactive, not-applicable, and coverage gaps: not_applicable with no speed pin. Gaps report unresolved root or hips roles, clips too short to sample stance, unavailable root-motion speed, or unresolved foot or toe roles.
  • Remediation and boundary: fix foot plants, root travel, or the declared speed pin in the DCC/export path and then review the runtime blend setup. This heuristic check intentionally does not auto-rewrite contact or infer movement ownership.
  • Runtime and references: failed stance motion shows up as skating or slipping feet in motion or blends. See Feet slide within one clip and API: FootSlide.

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.

Machine-Readable Output

animsmith's native JSON is the stable source of truth for pipeline adapters. Text and Markdown lint output are presentation views over the same evaluation results. The HTML report renders the same typed findings, coverage gaps, and prediction facets beside its sampled-motion view.

HTML report: source poses and illustrative blending

The single-document report keeps source playback separate from its optional illustrative blend. Source panes draw the exact Rust model-space pose-grid positions judged by checks. The illustrative blend control appears only after the selected pair passes admission, and adds a third pane computed from the embedded sampled-local-trs-blend-v1 presentation authority. It produces no finding, metric, animation export or engine-runtime conformance claim. The separate before/after comparison report keeps its existing source-pose and overlay behavior.

A finding whose clip name is missing or ambiguous shows a refusal notice and leaves the source selection and frame unchanged. A uniquely named finding still selects its source clip and judged time.

Samples and blend math

Each included, admitted clip carries a dense local-transform stream alongside its source positions: frame-major, then bone-major, ten little-endian f32 components per record in tx, ty, tz, qx, qy, qz, qw, sx, sy, sz order. Existing parent indices identify roots or earlier bones. Rust sampling has already filled absent channels from the rest transform; malformed channel storage is not treated as an absent channel. No rest-pose copy or authored animation tracks are added to the report.

At shared normalized phase u, each clip selects its own nearest frame, round(u * (frames - 1)), clamped to its grid. The primary clip drives playback; both actual source times are labelled. This selects sampled frames and does not resample or time-warp authored clips. Pairing is available across admitted clips of the document and does not require gait-group membership. Name-based links resolve only unique names; an invalid or self-pair clears pairing.

At an interior weight w, translation and scale use componentwise (1-w)*a + w*b. Rotations use shortest-hemisphere normalized linear interpolation (nlerp): normalize both quaternions, negate the second only when their dot product is negative, retain its sign at an exact zero-dot tie, then normalize the weighted sum. These calculations use binary64 arithmetic. Full column-vector affine matrices compose T * R * S, with each root's own transform and world[parent] * local[child]; this retains shear from rotated nonuniform-scale ancestors. Finite zero and negative scales are allowed.

Weights 0 and 1 instead draw the corresponding source positions exactly. This explicit endpoint branch guarantees agreement with the source panes; it does not promise a universal continuity or imperceptibility bound for arbitrary scaled chains. Interior results must remain finite and representable as f32 before GPU upload; unavailable results clear the prior blend instead of displaying stale geometry. The binary64 Rust reference uses the same embedded f32 samples as the browser. Its synthetic-fixture conformance tolerance is 1e-5 + 1e-5 * abs(reference) metres per position component, not an engine acceptance threshold or a proof for arbitrary skeletons.

Added-data limits and omissions

The new local-authority path admits at most 1,024 bones, 4,096 included clips, 65,536 aggregate track-metadata records and 32 MiB of added raw local data (40 bytes per bone/frame, at most 838,860 complete records). Checked aggregate counts and byte arithmetic precede local-value inspection and stream allocation. The aggregate base64 allocation is also checked. Budget excess or arithmetic overflow omits all local streams, rather than keeping whichever clips fit first. Source playback, findings and report success remain intact.

After aggregate admission, malformed track targets, properties, storage, cardinalities or duplicate channels omit that clip's complete local stream. Non-finite sampled components or quaternion length outside 1 +/- 1e-4 do the same. Invalid shared parent topology makes all blends unavailable. Each refusal shows a reason; source findings remain available. The browser validates and decodes only the selected pair when selection changes, before showing blend controls, and retains at most two local streams. Weight, phase and repaint do not decode streams or scan the clip array. Reusable storage supports one O(bones) blend/FK pass per repaint, with no all-pairs table or blended trail.

This cap bounds added local data and its processing, not existing source pose allocation, sampling, checks, loader/caller data, full JSON or full HTML memory. render(inputs) -> String keeps its existing public API. The comparison's separate 32 MiB pose budget remains per side and is unchanged.

All three panes use the same fixed source-based camera. Interior blended rotations can extend outside those bounds; use the existing orbit/zoom controls. There is no guarantee every interior pose fits the initial view and no weight-dependent automatic refit.

The persistent caption reads:

Engine-agnostic illustrative blend of sampled local transforms (nlerp rotations). Normalized phase, not a time warp. Not Bevy, Unity, Unreal or Godot runtime evidence.

--evidence-only omits both source positions and local transforms and skips local-authority work. Both are motion-bearing data; neither belongs in a shared report when the source-motion license forbids it. The notice reads:

Pose playback and illustrative blending are omitted in this evidence-only report. No sampled positions or local transforms are embedded.

Skeleton compatibility

animsmith skeleton compare emits one immutable skeleton-compatibility-v1.schema.json (urn:animsmith:schema:skeleton-compatibility:1) result. Its control plane is the exact raw correspondence TOML bytes, recorded as correspondence.input. That TOML pins the exact source and target primary identities, their explicit selected node-name sets (including each selected root), the declared matching mode, the resolved explicit mapping (empty for exact-name mode), and every tolerance. It does not infer skeleton membership from scene graph reachability. Invalid or stale control input, unreadable input, or an input beyond the 64 MiB primary-source bound is an operator error (exit 2) and emits no result.

Each subject records selected_skeleton_identity: the exact identity of a canonical, name-ordered projection of the selected source-node measurements and selector. This is provenance derived from the loaded asset, not another authored rest-pose profile.

The result keeps structural topology/rest evidence separate from optional source skin-membership, inverse-bind, and deformation-model facets. A result is compatible only when every required topology/rest row was available and within its declared tolerances. Complete mismatches are incompatible; mixed available/unavailable required topology/rest evidence is partial even when another retained field also mismatches; and no evaluable correspondence is not_evaluated. The last three exit 1. Neither the outcome nor any optional facet establishes retargeting, runtime deformation, masking, contact, gameplay, or artistic acceptance.

rows are deterministic: correspondence-covered source rows are emitted in source-name order, followed by unmapped source names and then unmapped target names in their respective name orders. A matched row contains separate local and rest-world translation, rotation, and scale deltas; the world rotation value exists only where the existing source measurement proves a right-handed unit-orthonormal rest-world linear transform. A child-bone ratio value exists only when both corresponding parent/child world positions permit an independent nonzero length comparison. Parent correspondence and child length use the nearest selected ancestor, walking across unselected intermediate nodes; they compare the declared selector projection rather than claiming that the complete raw hierarchies are identical. missing_source, missing_target, and parent_mismatch rows are explicit rather than folded into a score.

Non-pass rows identify a deterministic owner_surface and remedy_class. These route work to the correspondence, selected skeleton authority, selected skin/bind evidence, or a documented measurement boundary; they do not infer project, artist, or vendor blame from structure alone. Unavailable source and target evidence sides carry the same routing fields. The text view renders the exact source, target, selected-skeleton, and correspondence identities; matching mode and explicit pairs; tolerances; every retained row delta; facet details; and remediation routing. JSON remains the stable adapter contract.

Transition-pose evaluation

animsmith evaluate-transition-poses INPUT --format json emits exactly one immutable urn:animsmith:schema:transition-pose-evaluation:1 result described by transition-pose-evaluation-v1.schema.json. It is neither an output-v11 measure/lint envelope nor a check stream. The result binds the exact input document bytes as subject_input, the complete exact config source as declaration_input, and the declaration's independent normalized identity as declaration_normalized. For configured document families it also binds the complete same-load dependency-closure identity at the subject and each member, so a changed external glTF animation buffer cannot be masked by unchanged primary JSON bytes. With no selected or ambient config, the exact declaration source is the defined zero-byte TOML sequence; an explicitly empty config is intentionally identical.

An absent or empty [transition_families] table is a complete passing result with reason: "no_configured_families". Complete configured passes exit 0; findings and every retained incomplete family exit 1 with this same result. If same-load dependency-closure capture is incomplete, configured families are retained as incomplete/not_evaluated with dependency_closure_incomplete; an empty declaration remains the complete no_configured_families pass because it evaluates no source data. Invalid config/declaration, contradictory witnesses, input/load failures, and serialization failures emit no result and exit 2. Because this result has no sidecar or previously published artifact, failed stdout delivery is also an operator error (exit 2), rather than a successful result with a best-effort diagnostic. V1 evaluates one loaded document and does not imply collection source reload, graph generation, engine runtime behavior, or input mutation.

animsmith collection evaluate-transition-poses COLLECTION.toml --families TRANSITION_FAMILIES.toml --format json uses the same immutable result schema, with subject_input bound to the exact collection manifest bytes. It verifies the envelope's manifest identity and every logical/source/take witness before source work. Missing, unreadable, oversized, digest-mismatched, malformed, or otherwise unusable source input makes its whole family incomplete/not_evaluated with member_unavailable; it never compares a surviving subset. A readable raw source retains its exact source_input even when unusable; only a source for which no complete bytes were available uses source_input: null, and only in a member_unavailable family. Per-family members may span documents but must have one exact normalized skeleton basis.

Collection lint

animsmith collection lint COLLECTION.toml --format json emits the separate current urn:animsmith:schema:collection-output:11 envelope. Historical urn:animsmith:schema:collection-output:9, urn:animsmith:schema:collection-output:8 (see collection-output-v8.schema.json), 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 envelopes remain immutable. It binds the exact manifest bytes to canonically ordered source, logical clip, and runtime-set records while preserving each set's declared member order. Every available source embeds its ordinary one-file output-v19 lint result; each established logical clip separately carries the existing ClipMeasurements value selected by raw source take index and exact authored take name, then mapped through the loader's observed normalized clip index. This duplicate-safe indexed projection does not revise the historical, immutable measurements-v15 name-keyed wire contract; collection-output-v11 embeds measurements-v18 in its nested output-v19 documents. The strict reader also preserves collection-output-v10 (urn:animsmith:schema:collection-output:10) with its exact nested output-v18 (urn:animsmith:schema:output:18) and measurements-v17 (urn:animsmith:schema:measurements:17) pair. The strict reader continues to bind historical collection-output-v9 only to output-v17 with measurements-v16, historical collection-output-v8 only to output-v16, historical collection-output-v7 only to output-v15, historical collection-output-v6 only to output-v14, and historical collection-output-v5 only to output-v13.

Source input, digest pin, config, loader, take inventory, and document-result states stay orthogonal. A readable digest mismatch can therefore retain its observed identity, loader result, and take drift without authorizing the clip binding. Missing/unreadable sources, rejected readable bytes, digest/take mismatches, and unestablished members produce a complete typed envelope and exit 1; manifest, rooted-path, selected-config, serialization, and tool errors exit 2 with no envelope. The frontend preflights the complete control plane before source execution and reads sources sequentially.

Collection dashboard

animsmith collection dashboard --collection COLLECTION-OUTPUT.json -o DASHBOARD.html --authority DASHBOARD.json consumes only a strict current collection-output V11 document. It writes a self-contained offline HTML presentation and the separate urn:animsmith:schema:collection-dashboard:1 machine authority. That authority records the exact raw collection-output identity, bounded source/take/logical-clip and ordered runtime-set projection, and optional compatible transition-pose evaluation input/result. The optional result must be transition-pose-evaluation V1 bound to the same manifest input; other evaluation contracts remain separate rather than being guessed into a per-member dashboard projection.

Every declared source has its own inventory row even when no logical clip binds to it. The row retains its safe locator, optional observed input identity, availability, loader state, dependency-closure state, and bounded observed-take inventory. Each physical take preserves its exact source-owned take index/name, normalized index/name state, evidence, and outcome without inventing a logical ID. Summary evidence totals count physical rows plus unscoped source findings and unscoped source-level required_prediction_unavailable facets; the logical-clip outcome partition remains a separate declared-row view. Valid nested findings that have no matching clip are retained as source-level unscoped findings and included in the dashboard finding total; the projection never assigns them to an arbitrary clip. Required-unavailable prediction facets without one exact physical witness are likewise retained once at source scope with their reason set rather than being guessed onto a take or logical row.

Nested check evidence is normalized-name addressed. If one source has that same normalized name at multiple physical indices, neither index is a unique witness: the physical rows carry duplicate_normalized_clip_name, name-addressed findings are counted once at source scope, and no findings, gaps, or predictions are guessed onto either physical or logical row. An established logical row must instead identify exactly one source-owned established physical row by source, source-take index, and take name, and its evidence/coverage/outcome must match that row during strict readback.

When transition evidence is present, a member's optional logical_clip is required exactly when its source_input, take_index, and take_name resolve uniquely through the dashboard source identities and logical declarations. Strict authority readback rederives the match rather than trusting the stored logical ID; absent, mismatched, and ambiguous witnesses remain unresolved. Runtime-set gaps and lifecycle are also rederived during strict readback from the declared member rows: each non-established member contributes one member_unavailable gap in member order, and the set is complete only when that derived gap list is empty.

The dashboard is inventory and evidence navigation only. It has no quality score and does not establish engine loading/playback, retargeting, contacts, visual or artistic quality, or gameplay acceptance. Caller-supplied per-asset report links must be exact logical-id keyed safe relative paths; source paths, licensed assets, and motion samples are never embedded.

Collection directional-speed evaluation

animsmith collection evaluate-directional-speed --policy POLICY.toml --evidence COLLECTION-OUTPUT.json --format json consumes only strict bounded collection-directional-speed-policy:1 TOML and collection-output V7, historical V6, or historical V5 JSON. It emits the JSON-only immutable urn:animsmith:schema:collection-directional-speed-evaluation:1 result described by collection-directional-speed-evaluation-v1.schema.json. The result identifies the raw policy and evidence bytes, retains all declared members in manifest order, and records incomplete/not-evaluated outcomes or declared-policy findings without evaluating a subset. Control-plane failures write no result and exit 2; result-bearing incomplete, not-evaluable, and finding outcomes exit 1; a complete passing policy exits 0.

Runtime-set evidence concludes only whether every declared membership and required indexed measurement was established. Its decision is always not_evaluated; it does not infer blending, synchronization, retargeting, controller policy, engine behavior, or artistic/gameplay readiness. A gait-group additionally projects each member's raw gait_phase availability (and phase when measured) onto that existing manifest-ordered member row. Its set-level evidence.gait_phase records lifecycle and members_measured; only a complete group (every declared member established and phase-measured) carries phase_spread and the explicit basis max_circular_deviation_from_mean. The scalar is the existing circular_phase_spread value, not a smallest covering arc, and is calculated from logical-ID-sorted phases even though member rows retain manifest order. Incomplete groups keep every member visible and omit both scalar fields; non-gait sets omit gait-phase evidence. Every declared member also carries raw root_travel: duration, translation availability, signed horizontal X/Z displacement, sampled horizontal travel, and speed availability/value. Set-level evidence.root_travel counts only members with every required raw fact and is complete only when all declared members are fully measured; it never reduces the set or adds direction, ratios, thresholds, or policy. The strict reader applies a 256 MiB N+1 cap before JSON decoding, validates current nested output-v19 plus historical collection-output-v9/output-v17, collection-output-v8/output-v16, collection-output-v7/output-v15, collection-output-v6/output-v14, and historical collection-output-v5/output-v13 through their existing reader, recomputes all summaries/work/set lifecycles, and rejects unknown fields or contradictory identities and states. Producer and reader also freeze 1 GiB per primary source, 16 GiB aggregate primary reads, and the collection-manifest V1 row/member/work limits. Derived normalized clip names allow at most 4,101 bytes: the 4,096-byte authored-name bound plus # and the largest duplicate ordinal permitted by the 4,096-clip manifest bound. Available nested measurement keys retain output-v19's 4,096-byte bound. If such a derived name cannot fit the immutable 4,096-byte text bound of the nested output-v19 contract, indexed clip measurements and physical binding are retained, but the nested document and its name-addressed check reference are nested_output_unavailable; the collection exits 1 instead of publishing schema-invalid nested JSON. After the aggregate N+1 witness is retained, later declared sources remain in the envelope as aggregate_exhausted with zero inspected bytes; they are not opened and cannot increase the terminal counter beyond N+1.

glTF animation addressability

animsmith generate addressability INPUT emits a separate, one-file V1 contract with immutable identity urn:animsmith:schema:gltf-animation-addressability:1. Its retrievable schema is gltf-animation-addressability-v1.schema.json. It is not an output-v11 measure/lint/diff envelope, and the two roots reject one another on readback. The staged reader accepts at most 256 MiB and applies that byte cap before UTF-8 or JSON decoding.

The root contains the normal bounded tool identity, the exact primary input identity, an engine-neutral inventory, and a nullable bevy adapter. The inventory retains the complete existing dependency-closure record plus raw source-order animation and channel observations. Source names are optional, non-unique metadata; source indices remain the identity authority. The inventory's canonical identity excludes tool metadata and the optional adapter, so selecting a profile does not change neutral evidence.

With no engine profile or another supported non-Bevy profile, bevy is null. For the exact Bevy revision 1 / 0.19.0 / gltf-asset-loader profile, it embeds the same-load prediction-provenance V1 record and the unchanged engine-addressability check evaluation. The evaluation's available facet subjects are the authoritative Animation{source_clip_index} display selectors. A required-unavailable facet remains blocking and makes the command exit 1. Incomplete dependency closure is retained honestly but does not, by itself, become a claim about runtime load success.

The contract is animation-only. It makes no scene, default-scene, skin, target-path or UUID, Bevy named-map-winner, extension-support, successful-load, target-survival, or animation-graph claim. JSON is canonical; --format text and --format markdown escape and render the same typed value without adding conclusions.

Rich glTF addressability v2

The richer producer is a separate immutable contract, urn:animsmith:schema:gltf-addressability:2; its schema is gltf-addressability-v2.schema.json. It preserves the V1 animation inventory and adds bounded same-load scene, node, skin, attachment, scene-path, default-scene, named-map, and animation- target evidence. V1 readers and the V1 Bevy revision-1 profile remain unchanged; selecting the richer exact Bevy path selects V2 rather than widening the historical root.

The raw inventory has independent coverage for scenes, nodes, skins, attachments, and path candidates. Complete empty domains prove absence; partial prefixes and unavailable domains never prove absence. Rows retain source-array indices, authored names, parents and child order, ordered scene roots, ordered skin joints, node-to-skin attachments, and all-scene root-to-node path candidates. An authored skin.skeleton is retained as an explicit source observation. Bevy 0.19 ignores that member, so V2 does not replace it with an inferred Bevy root. The report does not claim scene instantiation or SkinnedMesh attachment.

The exact Bevy adapter is paired with the existing one engine-addressability evaluation and reuses the V1 Animation{i} selector primitive. It is pinned to profile revision 3, Bevy v0.19.0, commit c6f634ca9f406d68ba5109d921247b654cb42c10, bevy_gltf 0.19.0, locked gltf 1.4.1, and commit-pinned label, loader, path, animation-target, feature, and root Cargo.lock sources. Scene{i} is emitted for each declared source scene; Gltf.default_scene is only a route to an existing Scene{i}. There is no DefaultScene label and no fabricated Scene0.

Bevy creates Skin{i}/InverseBindMatrices eagerly for every declared source skin, including unreferenced skins; an absent inverse-bind accessor uses the identity fallback. Skin{i} is materialized when any source node references the skin during Bevy's all-source-node construction pass. Source skin indices, not the order of Bevy's collected skin values, are identity. Named scene, animation, and skin maps are separate from typed labels and use source-order last-write-wins semantics (the skin map follows lazily created skins in first- reference order).

Target projections are per unique source animation target node and retain contributing animation/channel identities. Paths use authored node names, including an authored empty string exactly, or GltfNode{source_index} fallbacks, exclude the scene world-root name, and are projected only when reachability, hierarchy, dependency closure, feature settings, and collision checks are complete. A duplicate full path, multiple scene candidates, target-ID collision, unreachable target, missing bevy_animation, disabled load_animations, missing pointer width, or incomplete evidence is typed required_unavailable; no guessed path or UUID is published. Bevy's target-ID reproduction requires an explicit 32- or 64-bit pointer width because its segment lengths use target-width little-endian encoding; the host width is never inferred.

The projection also carries target_coverage, independently of the retained target rows. A target domain is complete, including when empty, only when the raw node/scene/path and animation/channel inventories are exhaustive. Partial or unavailable evidence makes coverage required_unavailable; a domain beyond the 4,096 row limit additionally reports target_domain_truncated, and an aggregate rich-projection limit uses projection_bounds_exceeded. This keeps positive-prefix target rows from being mistaken for proof of complete, collision-free coverage.

Each new rich projection domain is capped at 4,096 rows. Its aggregate structural references are capped at 65,536 and its dynamic projection text at 1 MiB; the sealed embedded V1 animation inventory and CheckEvaluation scopes retain their own bounds. Names and path segments are capped at 1,024 UTF-8 bytes, paths at 4,096 bytes and 256 segments, and the staged report reader at 256 MiB. Target coverage is an explicit projection: a retained canonical prefix is target_domain_truncated, while any new rich projection bound overflow is projection_bounds_exceeded; no retained target is treated as collision-free after truncation. Strict readback rejects N+1 collections and contradictory identities or states. A second bounded parse of already captured primary bytes is allowed within the same loader invocation, but no primary/dependency reopen is allowed. V2 remains prediction evidence only: it does not certify runtime loading, target survival, graph wiring, scene spawning, or playback. Required-unavailable prediction retains the existing exit-1 convention; malformed tuple, configuration, or input errors remain exit 2.

If the resulting single-check prediction would exceed core's per-file facet budget, the adapter compacts its unavailable facets to one subjectless engine-addressability:facet-budget scope with facet_budget_exceeded. This is a user-visible compaction marker, not a new check or a claim that omitted facets were available.

Engine import advice

animsmith generate import-advice INPUT emits a separate one-file V1 contract with immutable identity urn:animsmith:schema:engine-import-advice:1. Its retrievable schema is engine-import-advice-v1.schema.json. It is neither output-v11 nor the glTF addressability root; strict readers reject the other shapes. The staged reader applies a 256 MiB byte cap before UTF-8 or JSON decoding.

Every record embeds exact PredictionProvenanceV1: the frozen engine-profile fact bundle, fully materialized settings, authoritative source format, raw source facts, primary identity, and dependency closure from the same load. Each available clip row binds its source-order clip index to one normalized document index and carries the original source-name observation separately from the normalized name. It also retains explicit project loop/movement intent plus duration, speed, loop-endpoint, and declared-frame-grid measurement/status pairs. Advice identity is a domain-separated canonical digest of provenance identity, lifecycle, clip rows, and importer payload; tool build metadata is excluded.

For Unity 6000.3 Generic and Humanoid, the payload projects only settings already materialized by the frozen profile: Model Importer Convert Units, Bake Axis Conversion, Generic Root Motion Source, and each clip's lockRootRotation, lockRootHeightY, and lockRootPositionXZ. bake maps to true; extract maps to false. Unreal 5.8 and Godot 4.7 revision 1 have no modeled setting vocabulary, so they emit state: refused with profile_settings_unmodeled and exit 1 rather than guessing. Incomplete raw clip inventory, missing source-to-normalized linkage, or mismatched measurement/settings evidence likewise refuses without exposing a prefix.

V1 does not emit frame-number ranges, sample rates, unit conversions, root-motion predictions, or project-file mutations. Source seconds multiplied by a floating-point FPS are not authoritative authored frame coordinates. Available advice exits 0, typed refusal exits 1, and configuration, profile, format, input, or serialization errors exit 2. Text and Markdown are escaped presentation views; JSON remains the contract.

Revision-2 import-setting advice

The same command also emits the immutable V2 contract urn:animsmith:schema:engine-import-advice:2, described by engine-import-advice-v2.schema.json. It keeps the output-v15 tool, V4 prediction-provenance, and V4 prediction-basis types by reference, while adding only the V2 basis and optional native projection fields. The lifecycle is closed against the same-load dependency closure: complete closure requires available, the exact projection, and no refusal_reason; partial or unavailable closure requires refused, dependency_closure_incomplete, and no projection. Unknown tuples, missing settings, and unsupported formats are configuration errors rather than advice documents, so they cannot be recast as a guessed projection.

Only these exact profile/input tuples can produce an available V2 projection:

tupleaccepted inputprojection
godot / 2 / 4.7 / resource-importer-sceneglTF JSON or GLBgodot_params: animation/fps (1..120; default 30) and animation/trimming (default false)
unreal / 2 / 5.8 / fbx-importerFBXunreal_fbx_import_data: explicit sample_rate as default_30, source_determined, or custom_hz(1..48000)

The projection is bounded to those documented keys and retains each value's explicit_config or profile_default origin. For Unreal, default_30 maps to bUseDefaultSampleRate=true; source_determined maps to false with CustomSampleRate=0; a custom rate maps to false with its explicit hertz. This is a deterministic same-load parameter projection only. It does not execute an engine importer, read back an imported asset, infer frame ranges, units, skeleton/retargeting, compression, root motion, runtime behavior, or write project files. The historical V1 contract and its refusal semantics remain immutable.

Contract identities

Validation and comparison JSON commands emit output contract v19 with the current protocol identity urn:animsmith:schema:output:19. Output-v17 retains identity urn:animsmith:schema:output:17 and remains retrievable historical schema evidence at output-v17.schema.json, paired with urn:animsmith:schema:measurements:16. Output-v16 remains retrievable historical schema evidence at output-v16.schema.json; its repository URL is a retrieval location, not the protocol identity urn:animsmith:schema:output:16. Output-v11 remains immutable historical schema evidence, and the measurement reader retains its validation paths for existing reports while CLI producers emit output-v19. urn:animsmith:schema:output:15, urn:animsmith:schema:output:14, urn:animsmith:schema:output:13, urn:animsmith:schema:output:12, and urn:animsmith:schema:output:11 remain immutable historical contracts and are never retargeted.

The current CLI emits output-v19. The Bevy revision-3 track-support slice continues to use V5 prediction provenance/readback with the bounded raw animation/channel inventory and gate outcomes described below. Unity Generic root-motion uses V6 prediction provenance. This does not retarget or invalidate output-v16, output-v15, or output-v9; all historical readers remain readable and their behavior remains preserved.

Those immutable prediction contracts consume measurements-v16. In output-v19, their basis values are validated against a deterministic measurements-v18-to-v16 evidence view: fully measured loop-continuity rows omit the V17-only per-bone status, while any unavailable bone projects to the historical unavailable outer fact; every V18-only rotation_xyzw fact is removed from skeleton and skin linear measurements. The output-v19 report still retains the complete measurements-v18 rows. This projection cannot turn unavailable evidence into measured evidence, and a projection or basis mismatch is rejected rather than weakening the prediction.

Output-v15 retains the V3 provenance/prediction path for revision-1 profiles and adds prediction-provenance:4, engine-profile-facts:2, resolved-engine-settings:3, raw scene/attachment inventory V1, machine results V1, and engine-prediction:4 for revision-2 profiles. One file cannot mix the two revisions. Settings retain at most 4,096 canonical clip rows. A 4,097th clip records partial coverage and bounded work; the identity commits to that state, and lint emits required-unavailable evidence (resolved_settings_overflow) instead of treating the retained prefix as a complete V1 inventory.

Measurement evidence is nested and independently versioned as urn:animsmith:schema:measurements:18. Its retrievable schema is measurements-v18.schema.json. Version 18 adds rotation_xyzw to every LinearTransformMeasurements surface exactly when its classification is unit_orthonormal. It is the canonical XYZW quaternion for the linear matrix's proper-orthogonal polar factor (the nearest proper rotation): the classifier deliberately admits small scale and shear residuals, which a quaternion does not encode. Exact rotations therefore recompose directly; tolerance-admitted residuals retain their matrix evidence alongside this orientation fact. Extraction uses all matrix branches near half turns, and the largest absolute component (XYZW tie order) is always non-negative, so equivalent q and -q rotations compare identically. The field is absent for uniform-scaled, non-uniform, sheared, reflected, singular, and non-finite transforms. Version 17 adds explicit availability to every per-bone loop-continuity row. Version 16 (urn:animsmith:schema:measurements:16), preserved at measurements-v16.schema.json, added per-primitive measurements in source primitive order. Each primitive records its nullable source material index, total decoded POSITION row count, and finite row count. Indexed primitives count each stored position once, not once per index reference. Primitive AABBs and centroids are finite-only and omitted when the finite count is zero. Version 16 also records leading_magic_hex only for unsupported, nonempty image payloads: at most the first 16 bytes as lowercase hex. It is bounded evidence, not a guessed format classification. urn:animsmith:schema:measurements:15 remains immutable historical evidence and has no primitive measurements or leading magic field. Version 15 added canonical per-bone TRS channel coverage and root-trajectory evidence. Version 14 introduced a sibling _availability status (measured, not_applicable, or unavailable) alongside its optional value field, so a consumer can distinguish "this clip has no subject for the fact" from "the fact applies, but derivation failed" — a distinction a bare optional value cannot express. Version 13 retains each per-joint source-declaration inverse-bind matrix beside the observations derived from it, refuses non-affine sources, and publishes a scale-free reciprocal infinity-norm condition number before trusting an inversion. Measurements v14 and earlier, and output v10 and earlier, remain immutable historical contracts. The historical output-v10 identity is urn:animsmith:schema:output:10. Output v9 first paired measurements-v15. Output v10 retained it and added the prediction-provenance substrate described below. Output v11 retains those contracts and adds role-resolution provenance: each delivered resolved bone name remains in rig.resolved_roles, while the matching policy appears in the parallel rig.resolved_role_policies map. Its rig.resolution_outcome is one of resolved, coverage, ambiguous_exact_match, ambiguous_folded_match, role_collision, or ambiguous_profile. Built-in profile fallback is only unique ASCII case-insensitive matching; explicit [rig.roles] entries remain exact and use explicit. Any future nested measurement revision will likewise require a new outer identity.

convert --format json is deliberately a separate conversion-evidence contract, not another command in the output-v11 envelope. Its immutable identity is urn:animsmith:schema:conversion-evidence:2; its retrievable schema is conversion-evidence-v2.schema.json. This lets producers pin conversion provenance independently of measurement and lint evidence.

An asset-property refusal from convert, assemble, generate contact-fragment, or collection transform-foot-cycle is not success evidence. Under --format json it uses the separate immutable urn:animsmith:schema:producer-refusal:1 contract, whose retrievable schema is producer-refusal-v1.schema.json.

This keeps conversion evidence v1/v2, assembly evidence v1-v7, output v1-v9, and scale evidence v1-v5 immutable. The record has outcome: "rejected", a null result, the command, and a typed {stage, kind, detail} rejection.

assemble writes a separate character-assembly-evidence v7 document to its required --evidence path, and prints the same record to stdout under --format json. Its immutable identity is urn:animsmith:schema:character-assembly-evidence:7; its retrievable schema is character-assembly-evidence-v7.schema.json. The paired GLB and evidence are prepared before publication, so an operator failure emits neither new destination and restores any prior pair.

scale writes scale-evidence v4 for glTF/GLB and the separate v5 FBX rest/bind record for the narrow FBX path to its required --evidence path, and prints the same record to stdout under --format json. Their immutable identities are urn:animsmith:schema:scale-evidence:4 and urn:animsmith:schema:scale-evidence:5; their retrievable schemas are scale-evidence-v4.schema.json and scale-evidence-v5.schema.json. The artifact and its evidence are prepared as temporaries and published as one pair, so a refusal or an operator failure emits neither destination and restores any prior pair.

Publication promotes the two temporaries with two renames, which are individually atomic but not atomic together. Only the artifact destination is moved aside first, so a process killed between the renames leaves the new artifact beside the previous evidence — a complete pair whose members disagree, which the evidence's own record of the artifact digest makes detectable. Backing the evidence up as well would turn that same window into a new artifact with no evidence at all. Both members are promoted from temporary files and therefore land with mode 0600 rather than the 0644 a plain create under the process umask would produce; this is shared by every producer that publishes a pair.

The --format json copy on stdout is a view of the pair, never a member of it. Both producers serialize their record exactly once and hand those same bytes to the evidence temporary and to stdout, so the file and the stream are identical by construction rather than by two serializers agreeing. Because the stream is not a destination, a stdout that cannot take it — a closed pipe, a full filesystem — changes nothing about publication and does not change the exit code: the failure is diagnosed on stderr and the outcome stands. Read the --evidence file, not stdout, when the record must be durable.

Conversion evidence v1 remains a historical immutable contract at urn:animsmith:schema:conversion-evidence:1. The current CLI emits v2 exclusively; regenerate v1 evidence when a v2 consumer is required.

Output-v2, output-v3, output-v4, output-v5, output-v6, output-v7, output-v8, output-v9, and output-v10 remain historical immutable contracts, as do output-v11 through output-v16. The current CLI emits output-v17; diff also retains strict version-matched historical readers, including output-v9. Regenerate an output-v14 report with a historical producer when a historical v14 artifact is required. Regenerate a current output-v19 report from the original asset with animsmith measure --format json when a current artifact is required.

Contact fragments

contact-fragment-v1.schema.json (urn:animsmith:schema:contact-fragment:1) is the strict, portable envelope for one selected clip's normalized contact facts. Its reader and RFC 8785 canonical serializer live in animsmith-core. animsmith generate contact-fragment is the strict V1 producer: it publishes a source-bound sidecar only after finite bilateral stance-support preflight and a complete dependency closure; typed refusals publish nothing. It reports sampled support, not physical contact or gameplay meaning.

The format-neutral core also implements the separately versioned contact-transform-result-v1.schema.json (urn:animsmith:schema:contact-transform-result:1) reader, canonical serializer, and exact trim/slice/resample/time-warp contact mapping. A result reader is given the separately supplied input fragment and an external context containing the current input/output artifacts, their complete captured closures, the expected producer, and any handler-produced extension outputs. It validates the closure-to-artifact bindings and independently rederives every event outcome and successful inline fragment instead of trusting copied result rows. Extension transformation requires exact schema/version-preserving output from an operation-specific handler; opaque payloads are never automatically copied, and unsupported extensions refuse the whole operation. This library contract does not mutate an animation asset or publish a collection generation directory.

Foot-cycle generation evidence

collection transform-foot-cycle publishes two strict success-evidence contracts outside the ordinary output envelope:

  • each members/NNNNNN/evidence.json is urn:animsmith:schema:foot-cycle-member-evidence:1;
  • aggregate-evidence.json is urn:animsmith:schema:foot-cycle-aggregate-evidence:1.

The aggregate is serialized once, staged as the generation member, and those same exact bytes are written to stdout after the no-replace directory publish. Its resource record includes itself: bounded serialization iterates the byte count to a stable exact value before publication, then verifies that the four component totals equal total_bytes and do not exceed 256 MiB. Member evidence binds the exact source/output/closure/contact identities and independent proof facts; the aggregate binds every deterministic relative path and member evidence identity in declared order. The retrievable schemas are foot-cycle-member-evidence-v1.schema.json and foot-cycle-aggregate-evidence-v1.schema.json.

A valid asset or capability refusal uses producer-refusal-v1.schema.json with command collection-transform-foot-cycle and exits 1. An operator failure emits no JSON and exits 2. The transformed contact fragments themselves remain the existing canonical contact-fragment:1 contract; the contact transform result is proof input and is not an extra generation file.

Foot-cycle failure diagnostics

Foot-cycle planner diagnostics use foot-cycle source preparation failed (<rule>), optionally followed by for member <logical-id>. Asset refusals carry this text in rejection.detail; operator failures carry it on stderr. The rule identifies which planner check failed without exposing source paths or nested parser errors. Logical IDs are bounded, validated collection IDs such as com.example/walk, not source-file locators.

The planner adapter names every current core error:

RuleMember IDRoute when received by the planner adapter
manifest-too-largeNoAsset refusal, exit 1
parameterization-too-largeNoAsset refusal, exit 1
too-few-membersNoAsset refusal, exit 1
too-many-membersNoAsset refusal, exit 1
invalid-slope-boundsNoAsset refusal, exit 1
invalid-proof-policyNoAsset refusal, exit 1
duplicate-memberYesAsset refusal, exit 1
duplicate-fragment-pathNoAsset refusal, exit 1
output-path-collisionNoAsset refusal, exit 1
missing-reference-memberNoAsset refusal, exit 1
manifest-mismatchNoAsset refusal, exit 1
runtime-set-mismatchNoAsset refusal, exit 1
wrong-runtime-set-kindNoAsset refusal, exit 1
member-order-mismatchNoAsset refusal, exit 1
evidence-count-mismatchNoAsset refusal, exit 1
too-many-contact-eventsNoAsset refusal, exit 1
too-many-contact-fragment-bytesNoAsset refusal, exit 1
evidence-member-mismatchNoAsset refusal, exit 1
non-canonical-fragmentNoOperator error, exit 2
fragment-clip-mismatchNoOperator error, exit 2
unsupported-contact-extensionNoAsset refusal, exit 1
invalid-detector-provenanceNoAsset refusal, exit 1
detector-policy-mismatchNoAsset refusal, exit 1
root-motion-binding-mismatchYesAsset refusal, exit 1
root-motion-evidence-unavailableYesAsset refusal, exit 1
root-motion-out-of-rangeYesAsset refusal, exit 1
invalid-contact-topologyNoAsset refusal, exit 1
topology-mismatchNoAsset refusal, exit 1
non-monotone-mappingNoAsset refusal, exit 1
segment-slope-out-of-rangeNoAsset refusal, exit 1
too-many-control-pointsNoAsset refusal, exit 1

This is the adapter inventory, not a promise that every error can occur after CLI admission. The bounded readers, declaration constructors, manifest-binding preflight, and evidence assembly reject or prevent some states before the planner runs. Their existing diagnostics, validation order, and exit routes remain in effect. For example, an invalid declaration still reports control; the tool does not postpone that validation to obtain a planner label.

Planner asset refusals retain stage analysis and kind asset-recipe-mismatch. The two operator errors in the table still emit no JSON. A future unrecognized variant from the non-exhaustive core error enum uses the closed compatibility label unknown-planner-rule and the same asset-refusal route; no current variant uses that fallback. The four member IDs above are retained from the core error, and no member is inferred for other rules. In particular, the topology and map errors currently carry no member.

Proof refusals retain stage proof, kind proof-failed, and the existing foot-cycle proof failed (<rule>) text. ArtifactPreflight, ClipMap, ContactBoundary, and LoopContinuity append for member <logical-id> for the prepared member being judged, including when several members select separate clips in one source. ArtifactPreflight covers both candidate document shape and writer preflight. Other proof diagnostics retain their existing text; collection-wide failures such as GaitSpread have no member attribution. A failure still prevents publication of the generation.

Common envelope

{
  "schema_version": 19,
  "schema": "urn:animsmith:schema:output:19",
  "tool": {
    "name": "animsmith",
    "version": "0.14.0",
    "source": {
      "revision": "0123456789abcdef0123456789abcdef01234567",
      "dirty": false
    }
  },
  "command": "measure",
  "summary": { "files": 1 },
  "files": []
}

tool.version is the package's plain semantic version. Source revision and dirty state are separate fields so automation never has to parse a decorated version string. Packaged source records its Cargo VCS revision and leaves dirty as null; builds without trustworthy VCS metadata may leave both fields null.

Every measure and lint file record also includes input, with the exact primary-file byte count and lowercase SHA-256 digest of the bytes parsed for that row. Retain this identity with the JSON evidence when a pipeline promotes or publishes an asset: it proves which primary payload the recorded result describes. For multi-file invocations, rows stay in argument order and each has its own independently calculated identity.

The input identity covers only the named primary input file. For profiled lint output, the sibling prediction provenance carries the bounded same-load dependency closure, including identities for captured external resources when coverage is complete. A primary-file digest alone remains insufficient closure evidence for text glTF or another source with external dependencies.

Operator failures do not emit a JSON envelope. They exit 2, write a diagnostic to stderr, and leave stdout empty. Content findings exit 1 at the configured threshold. Any required_prediction_unavailable facet also exits 1 and cannot be suppressed by severity or --allow; ordinary engine-neutral coverage gaps remain nonblocking.

convert and assemble asset refusals exit 1. JSON mode emits exactly one producer-refusal v1 document on stdout and leaves stderr empty; text mode leaves stdout empty and emits one escaped stderr line carrying the same stable kind. A consumer can therefore branch on exit plus stdout without parsing English. Failure to serialize a truthful refusal record is an operator error; failure to deliver already serialized bytes keeps exit 1 and is diagnosed best-effort on stderr.

convert

convert --format json emits one conversion-evidence v2 document. It records the input and output paths, the requested options, and counts derived from the written artifact. It is producer evidence: consumers should use the stable field names and schema identity rather than parsing the text write summary.

That contract is success-only. A rejected source instead emits the shared producer-refusal v1 record:

{
  "schema_version": 1,
  "schema": "urn:animsmith:schema:producer-refusal:1",
  "tool": {
    "name": "animsmith",
    "version": "0.14.0",
    "source": { "revision": null, "dirty": null }
  },
  "command": "convert",
  "outcome": "rejected",
  "result": null,
  "rejection": {
    "stage": "transform",
    "kind": "transform-refused",
    "detail": "cannot bake static transforms: document has no mesh instances"
  }
}
{
  "schema_version": 2,
  "schema": "urn:animsmith:schema:conversion-evidence:2",
  "tool": {
    "name": "animsmith",
    "version": "0.14.0",
    "source": { "revision": null, "dirty": null }
  },
  "command": "convert",
  "input": "prop.fbx",
  "output": "prop.glb",
  "options": {
    "animation_only": false,
    "bake_static_mesh_transforms": true,
    "material_texture_recipe": null
  },
  "artifact": {
    "nodes": 2,
    "animations": 0,
    "meshes": 1,
    "primitive_positions": 3,
    "materials": 1,
    "clips_without_writable_tracks": 0
  },
  "static_mesh_bake": {
    "entries": [
      {
        "source_node_index": 4,
        "source_node_name": "prop",
        "source_mesh_ordinal": 0,
        "source_mesh_index": 7,
        "source_mesh_name": "prop_mesh",
        "output_node_index": 1,
        "output_mesh_index": 0,
        "world_transform": [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
        "linear_determinant": 1,
        "primitive_count": 1,
        "position_count": 3,
        "normal_count": 3
      }
    ]
  }
}

static_mesh_bake is absent unless --bake-static-mesh-transforms was requested. Its entries are in deterministic source-node order. source_node_index, source_mesh_ordinal, and source_mesh_index identify the source record; names are display data and need not be unique. world_transform is the 16-element column-major rest world matrix applied to the source positions. linear_determinant records the accepted transform's linear determinant. The output node and mesh indices are indices in the generated artifact.

The static bake is opt-in and conflicts with --animation-only. It only accepts unanimated, unskinned, singly-instanced static geometry with finite, non-reflecting, non-singular (including near-singular) transforms. It bakes positions and inverse-transpose normalized normals into a canonical identity-root output while retaining indices, UVs, model-supported material assignments, and embedded base-color and normal textures. Unsupported input is an operator error, not partial evidence. Repeated same-platform conversion with the same input and options emits a byte-identical artifact.

When options.material_texture_recipe is a path, the top-level material_texture_recipe object is required. When it is null, that object is prohibited. This separates ordinary linked or embedded texture conversion from recipe provenance. The object records the recipe identity, declared recipe path and optional root, dimension cap, locked processor policy, and deterministic consumed/emitted image records:

{
  "schema_version": 1,
  "schema": "urn:animsmith:schema:material-texture-recipe:1",
  "path": "recipes/materials.toml",
  "texture_root": "textures",
  "max_dimension": 1024,
  "processor": {
    "image_crate": "image@0.25.10",
    "png_crate": "png@0.18.1",
    "jpeg_crate": "zune-jpeg@0.5.15",
    "base_color_algorithm": "sRGB-to-linear premultiplied-alpha Lanczos3",
    "normal_algorithm": "tangent-vector Triangle renormalize +Z fallback",
    "data_algorithm": "linear-channel Triangle",
    "output_encoding": "PNG RGBA8 compression=Best filter=NoFilter"
  },
  "consumed_inputs": [
    { "material_index": 0, "material_name": "surface", "slot": "base_color", "declared_path": "surface-base.png", "mime": "image/png", "dimensions": [2048, 1024] }
  ],
  "emitted_textures": [
    { "material_index": 0, "material_name": "surface", "slot": "base_color", "declared_path": "surface-base.png", "mime": "image/png", "dimensions": [1024, 512], "resized": true, "emitted_bytes": 1234 }
  ]
}

Each array has the required BaseColor and normal pair plus any declared metallic-roughness and occlusion slots for every recipe material. Records are ordered by source-material index, then BaseColor, normal, metallic-roughness, and occlusion; recipe declaration order does not affect them. material_index is the source-material identity; material_name is display data. emitted_bytes is the encoded byte count. See material texture recipes for containment and image semantics.

assemble

assemble writes character-assembly evidence v7 beside its GLB. The evidence binds the effective recipe and its SHA-256, every base/clip/recipe/texture input and digest, selected source takes and windows, exact track-operation counts, removed named-bone translation deltas, mesh and skin canonicalization, tool identity, and the final artifact digest and counts. When prune_constant_tracks is enabled, each clip also records the exact removed tracks in pruned_constant_tracks, including each track's index in the completed, normalized output clip immediately before pruning (the pre-prune authored order). In assembly evidence v3 through v7, bone_index is the BoneId in the post-canonicalization/pre-node-removal skeleton; removed_nodes provides the stable compaction ledger needed to derive a surviving final index. The array is empty when pruning is disabled or no track is removed. transforms.removed_nodes records the final structural projection in original pre-removal node order, with each node's name, original index, nullable original parent index, and whether it was selected directly. It is empty when the recipe selects no nodes. Paths remain operator-declared; canonical host paths used for containment checks are not serialized.

Assembly evidence remains success-only. A valid recipe that does not fit the loaded assets exits 1 before publication and uses producer-refusal v1 in JSON mode; invalid recipe syntax/schema/values or an input/path/publication I/O failure remains exit 2 with empty stdout.

When the optional recipe-v7 rest_bind_scale operation is active, evidence also pins the declared root name, every input's resolved root name/source-node index and applicable source-skin index, the effective staged source selectors and factor, the exact digest and versioned basis fingerprint for the base and every clip input, each semantic compatibility result, its rest-bind or skinless-clip-tracks application, and the shared scale proof over the exact staged artifact bytes. Full rest/bind rows name urn:animsmith:character-assembly-scale-basis:1; animation-only clip projections name urn:animsmith:character-assembly-skinless-clip-scale-basis:1 and omit the inapplicable resolved skin index. A captured clip source may carry a skin or mesh; its source_projection record remains raw-source evidence while the track-only scale projection excludes geometry, deformation, materials, and bind state before rebasing. For glTF/GLB, role-specific preflight retains framing, dependency, raw-coverage, named-skeleton, and animation accessor/layout safety. The stable skinless-clip-tracks value names the projected operation domain, not the raw source contents. Base and full rest/bind rows remain strict. residual_comparison_counts uses the same twelve stable field names as proof.residuals, keeping every measured maximum paired with the count from the shared proof API without changing immutable scale-evidence v4. V7 retains v6's captured input container and either its raw glTF preservation boundary or its normalized/baked FBX capability inventory and private staged-GLB identity. For FBX, that private stage may exclude an unskinned mesh instance inside a declared remove_nodes closure while the input digest and capability inventory continue to describe the captured raw source. Omitting the block omits this operation evidence and retains ordinary assembly behavior under the v7 envelope.

The normative recipe and evidence contracts are character-assembly-recipe-v7.schema.json and character-assembly-evidence-v7.schema.json. The recipe identity is urn:animsmith:schema:character-assembly-recipe:7; v1 through v6 remain immutable historical contracts. See multi-source character assembly for operation and consumer-boundary semantics. Migrate from v6 by selecting recipe/evidence v7 and replacing its two source indices with root_node_name; an omitted rest_bind_scale block retains the existing behavior, while v3 continues to reject that block as unknown.

scale

The scale workflow explains when to choose each operation and how the rewrite/reload/proof/publication transaction produces this record. This section is the wire-format authority.

glTF/GLB scale writes scale evidence v4 beside its artifact. One record serves both outcomes, discriminated by outcome:

outcomeresultrejectionPublishedExit
"published"the recordnullthe pair, atomically0
"rejected"nullthe typed reasonnothing1

A refused run's record is printed by --format json and is never written to the --evidence path: publication is an artifact/evidence pair, and a refusal has no artifact. This follows lint --format json, which prints its machine-readable result to stdout and exits 1 when the asset has a problem. For example, a finite negative primary skin weight is rejected during planning with rejection.kind: "negative-skin-weight"; the record has result: null and neither destination is written. Shared structural refusals likewise keep their specific existing kinds (such as invalid-parent, invalid-track-shape, or invalid-mesh-instance) rather than collapsing into a generic document-shape kind.

The record binds the operation and its declared selectors, the operator's declared paths verbatim, the input digest and byte count, and the complete raw capability manifest of the source. capability is null only for a refusal raised before an inventory existed — bytes that never parsed. A published record always carries the manifest, because publication is reachable only through a preflight that built one. In v4 that manifest includes the sorted JSON-pointer inventory of every static mesh/node and animated morph-weight source accepted for preservation. Static JSON weights retain their numeric values, while animated weight accessor payloads remain byte-exact. A published run adds the fixed tolerance policy by identity and in full, both observed-factor witnesses with the divergence between them and the ceiling the design expects of it, the affected node and skin identities in the raw source index space the selectors use, the rewritten model domains, proof coverage and results, the artifact-level residuals, and the artifact's own digest, byte count, rewritten accessors and rewritten JSON pointers.

Residuals are never published flat. Each is { "evaluated": bool, "max": number|null }, with max null exactly when the plan's obligations and the source's own payloads gave that claim nothing to check. A residual reported as 0.0 for a claim nothing evaluated would be a false record rather than a missing one, which Appendix D §D.6 forbids.

Every number in the record is guarded for finiteness before serialization. serde_json renders NaN and both infinities as null, which in a residual field would read as a checked-but-unmeasurable claim; the producer fails instead, and publishes nothing.

proof.read_back_digest_matches records the producer's own third artifact check: the staged artifact was re-read from disk and its digest compared with the bytes that were proved. The two frontend proofs already reload the candidate, re-run the shared core proof, and re-run the whole rewrite to byte-compare for determinism; what they cannot see is the write path, which is what this closes.

Paths are recorded exactly as the operator wrote them. Canonical host paths are computed only for publication-safety comparisons among input, output, evidence, and retained external dependency keys. They are deliberately not serialized, so identical arguments produce byte-identical evidence. Nothing in the record carries a timestamp.

The normative glTF/GLB contract is scale-evidence-v4.schema.json. The CLI emits v4 for glTF/GLB; immutable scale-evidence-v1.schema.json, scale-evidence-v2.schema.json, and scale-evidence-v3.schema.json remain available for historical records.

The narrow FBX scale rest-bind path emits v5, scale-evidence-v5.schema.json, and always writes a new .glb artifact. Its capability is the complete ufbx normalized-domain inventory, not a raw FBX accessor manifest. result binds the private staged GLB identity and then nests the v4-shaped exact GLB rewrite, reload/proof, and read-back record for the published candidate. This is a re-encoding proof, not a claim that FBX bytes, object properties, or authored curve keys were preserved. Source-aware admission can therefore allow user-defined properties and bounded external texture/video declarations when same-load evidence proves they do not affect rest/bind state. The frozen v5 record retains the inventory counts; the same-load loader source separately retains raw facts and dependency-closure coverage/identities, but v5 does not serialize those sidecars. Supported linked texture bytes are captured before staging so the operation does not silently remove them, without claiming raw FBX material/texture-assignment preservation. The inventory projection is frozen with v5: a new FBX fact requires a new evidence identity rather than silently changing this wire record. Whole-document FBX scaling remains refused.

A whole-document factor of one has no raw write set. Its v4 result.artifact.rewritten_accessors, rewritten_json_pointers, and reencoded_buffers arrays are empty, and result.proof.artifact.rewritten_accessor_count is zero. This factor-one behavior is carried forward unchanged from v3.

For rest/bind, result.domain_rewrites.scale_animation is true: every stored scale VEC3 is rebased by its topology multiplier — 1 / s at the selected root, 1 at strict affected descendants and unaffected nodes — including both cubic-spline tangent elements. It is false for whole-document conversion, which preserves dimensionless scale channels. The raw preflight also refuses an animation channel targeting a node authored with matrix as animated_matrix_node; no artifact or evidence file is written for that refusal.

For an artifact-proof-failed refusal from the exact raw-JSON-preservation walk, v2 added rejection.artifact_proof_differences: the artifact proof's bounded, deterministically ordered raw-JSON difference sample. items has one to 16 entries, each with a JSON-pointer location and an artifact_added, artifact_removed, or value_changed kind. omitted counts entries outside the prefix, so the full difference count is items.length + omitted. The field is null when that exact-preservation walk did not supply locations for the refusal. It does not replace rejection.violations, which continues to describe unsupported source capabilities.

measure and lint

Both commands put evidence under files[].measurements:

{
  "schema_version": 18,
  "schema": "urn:animsmith:schema:measurements:18",
  "clips": {},
  "mesh_definitions": [],
  "node_instances": [],
  "scenes": [],
  "skeleton_source_coverage": "unavailable",
  "skeleton_nodes": [],
  "skins": [],
  "material_resource_coverage": "complete",
  "material_definitions": [],
  "textures": [],
  "images": []
}

clips maps clip names to duration, frame count, animated bones, exact local TRS channel coverage, rotation ranges, optional per-bone loop continuity, and optional role-dependent gait, foot-seam, root-trajectory, and speed metrics. bone_channels is the canonical set of non-empty local channels present in the measured document:

"bone_channels": [
  { "bone_index": 0, "bone_name": "hips",
    "properties": ["translation", "rotation"] },
  { "bone_index": 3, "bone_name": "weapon_socket",
    "properties": ["scale"] }
]

Rows use skeleton-index order; properties use translation, rotation, scale order. Bone index is identity and the name is display metadata, so duplicate bone names remain distinguishable. Duplicate tracks for one (bone, property) pair collapse to one presence fact. This describes surviving normalized TRS coverage in the artifact regardless of which transforms were requested; it is not pruning evidence, track multiplicity, morph-weight coverage, or a raw source-channel inventory. animated_bones remains the sorted unique name projection for compatibility. Empty or structurally malformed channels do not contribute to bone_channels, animated_bones, or bone_rotation_range_deg; every rotation-range key is therefore also present in animated_bones.

mesh_definitions[].primitives is a source-order array. Each row retains the nullable source material_index, counts decoded base POSITION rows in vertex_count, and counts finite rows separately in finite_vertex_count. primitive_index retains the original zero-based source slot, so gaps are valid when a loader omits unsupported source primitives; normalized documents without source identity use contiguous retained-order fallback values. Indexed primitives count each stored position once; index references are not expanded or counted repeatedly. Primitive geometry_aabb and geometry_centroid use finite positions only and are absent when there are no finite positions. Mesh-level totals use the same finite-only geometry domain. For unsupported nonempty images, leading_magic_hex records at most the first 16 payload bytes as lowercase hex. It is evidence of the unsupported bytes, not an inferred or guessed image format; it is absent for supported images and other unavailable cases.

Each optional clip fact — loop_continuity, loop_endpoint_mode, frame_grid, loop_seam_ratio, gait (and its own gait.phase), root_trajectory (with independent nested translation and yaw facts), and speed_mps — carries a required sibling <field>_availability status (gait.phase uses phase_availability, nested beside phase inside the gait object) with one of three values:

  • measured: the fact was derived; the sibling value field is present.
  • not_applicable: this clip has no subject for the fact (for example, no declared loop, no resolvable gait roles, or no root/hips travel quantity); the sibling value field is absent.
  • unavailable: the fact applies to this clip, but it could not be derived (evidence was insufficient or a producer stopped emitting it); the sibling value field is absent.

The value field is present if and only if its status is measured; the schema rejects a measured status without a value and a not_applicable or unavailable status with one. This distinction matters because the two absences require opposite handling: legitimate non-applicability should remain acceptable, while an applicable-but-unavailable metric must not silently pass a consumer threshold that only checks for a present value.

Field-by-field applicability:

Fieldnot_applicable whenunavailable when
loop_continuitythe skeleton has no bonespresent bones exist, but the clip has fewer than three samples or the shared seam sampling grid is unusable; individual non-finite bones remain explicit unavailable rows when the shared grid is usable
loop_endpoint_modethe clip is not declared loop = truethe clip is declared loop = true, but neither the strict duplicate-endpoint predicate nor sampled continuity evidence can classify it
frame_gridthe clip has no declared/configured FPS expectationan FPS expectation is declared, but the duration or an authored key does not land on that grid
loop_seam_ratiothe Hips role or every foot role is unresolved, or the Hips + at least one foot role resolved but no real stride was found between the seam-adjacent frames (feet did not move relative to the hips by at least the configured stride floor) — a planted/idle clip has no stride subject to normalize the seam againstthe Hips + at least one foot role resolved and a real stride was found, but the ratio itself still could not be derived
gaitthe Hips role or every foot role is unresolvedthe Hips + at least one foot role resolved, but the per-clip cycle sample failed
gait.phase (nested phase_availability)only one side (left or right) resolved a foot role, or both sides resolved but the L-R foot-height signal has exactly zero peak-to-peak swing (so it has no phase subject)both sides resolved and the L-R foot-height swing was nonzero, but the fundamental-harmonic trough still could not be derived; current finite sampled production data has no known route to this defensive state
root_trajectoryneither the Root nor the Hips role is resolvedRoot (preferred) or the Hips fallback resolved to an index outside the measured skeleton, or its captured name no longer matches that index
root_trajectory.translation (nested translation_availability)never, once a trajectory bone is selectedthe shared metric grid is unavailable or any sampled selected-bone position is non-finite
root_trajectory.yaw (nested yaw_availability)never, once a trajectory bone is selectedthe shared metric grid is unavailable, sampled rotation is non-finite/zero, the fixed heading witness becomes vertical, or an adjacent sampled step is an ambiguous half turn
speed_mpsneither the Root nor the Hips role is resolvedthe Root or Hips role resolved, but root-motion speed could not be derived from the sampled grid

root_trajectory selects the resolved Root role whenever it exists. Hips is used only as source_role: "hips_fallback" when Root is unresolved; a bad or unmeasurable resolved Root never causes a silent Hips fallback. A valid selection keeps its bone_index, bone_name, and source role even when one or both nested metric domains are unavailable. Translation and yaw are derived independently, so bad positions do not erase usable yaw and bad rotations do not erase usable translation.

Collection lint projects the existing per-document duration_s, root trajectory translation availability and X/Z/travel values, plus speed_mps availability/value, into each declared runtime-set member's root_travel object. evidence.root_travel.members_measured counts only rows that have all of those raw facts, while its lifecycle is complete only when that count equals the declared member count. It retains manifest member order and does not infer a direction, reference member, ratio, threshold, finding, or controller policy.

The translation domain is normalized right-handed, +Y-up model space in metres: +X is right and -Z is forward. Signed X/Z and Y displacement compare the final sample to sample zero. horizontal_travel_m sums the XZ length of every sampled step, so horizontal out-and-back motion does not collapse to a zero fact. Vertical min/max are sampled signed displacements from sample zero, include the initial zero, and contain the endpoint displacement.

Yaw chooses at sample zero the positive local Z, Y, or X axis with the largest finite horizontal projection (ties prefer Z, then Y, then X), records that heading_axis, and holds the witness for the clip. net_yaw_deg is the endpoint-equivalent signed result in [-180, 180]; unwrapped_yaw_deg is the signed sum after shortest-step wrap handling; and yaw_travel_deg sums the absolute sampled steps so reversals do not cancel. An adjacent sampled step within 0.0001 degrees of exactly 180 degrees has no recoverable direction, so yaw is unavailable rather than guessed. Positive yaw increases atan2(x, z); for a +Z-aligned witness this rotates +Z toward +X, the positive right-handed direction about normalized +Y. A multi-step endpoint-equivalent result within the same 0.0001-degree tolerance of a half turn is canonicalized to signed +180 or -180 using the sign of the unwrapped result.

These are bounded regression facts from the same inclusive uniform MetricGrids grid used by checks and reports. The grid has the longest authored channel's key count and needs at least three keys and positive clip duration. It can alias fast or irregular motion between samples, and adding an unrelated denser track can change its resolution. Consequently travel, vertical extrema, winding, and yaw travel are sampled observations, not proof of continuous-curve extrema or of a transform's authored-data correctness. Legacy speed_mps retains its existing calculation and status.

diff reports every change to a clip fact's availability status — not only the not_applicable <-> unavailable transition that a bare optional value would otherwise compare as unchanged (both are an absent value), but also a measured <-> either absence transition, reported under the field's own <field>_availability metric name (for example "loop_seam_ratio_availability") so it never collides with that field's ordinary value-movement delta ("loop_seam_ratio", reported "appeared" / "disappeared" / "moved" as before). This matters most for loop_endpoint_mode and frame_grid, which carry an enum and a small object rather than a plain number: they have no value-movement delta of their own, so without the status delta a measured -> unavailable transition on either would be completely silent.

For clips declared with loop = true, loop_endpoint_mode is present when AnimSmith can distinguish a strict mechanically removable duplicate_endpoint, a non-duplicate unique_cycle within the effective position/rotation closure caps, or a non_closing cycle beyond either cap. Clips not declared as loops report loop_endpoint_mode_availability: "not_applicable"; loops without enough finite evidence report "unavailable". When a positive declared FPS places the duration and every authored key on its frame grid, frame_grid records that fps and the rounded number of frame_intervals; frame_count remains the longest authored channel's key count and is not relabeled as an authored FPS grid.

loop_continuity.bones[] is present when a clip has at least three samples and the shared seam sampling grid is usable. Rows stay in skeleton order and carry bone_index, bone_name, and availability; the numeric index is identity, while the name is display context and need not be unique. measured rows carry all four numeric fields below. unavailable rows carry none of them, so one bone's non-finite transform evidence cannot suppress usable evidence for other bones. not_applicable is not valid for an existing bone. Each measured row reports:

  • position_delta_m: last-to-first model-space position distance (C0);
  • rotation_delta_deg: last-to-first shortest-path model-space rotation difference (C0);
  • seam_velocity_delta_mps: difference between the model-space linear velocities entering the last sample and leaving frame 0 (C1).
  • seam_angular_velocity_delta_degps: shortest-path model-space angular velocity difference, in degrees per second, between those same incoming and outgoing steps (rotational C1).

The velocity comparison deliberately uses the two in-clip steps adjacent to the wrap. The uniform grid contains both t=0 and t=duration, so treating the duplicate last-to-first endpoint chord as a velocity would report zero on a perfectly closed loop. Model-space values include ancestor motion; the rotation chain is composed independently of scale rather than decomposed from a potentially sheared matrix. These measurements need no rig profile and are emitted for measurable clips whether or not project configuration declares them as loops. The loop-closure, loop-seam-vel, and loop-seam-rot checks judge them only where [clips.<name>] loop = true.

mesh_definitions contains one record per source mesh definition. Its geometry_aabb reduces finite primitive POSITION values in the mesh's own coordinates; it is independent of every node and scene. When finite positions exist, optional geometry_centroid is their arithmetic mean in that same mesh-local coordinate domain. Equivalently, it is the finite-count-weighted mean of the available primitive centroids; primitives with zero finite rows contribute neither weight nor position. It is not the AABB midpoint and does not weight vertices by triangle area, volume, or skin influence. Both fields omit non-finite positions and are absent when no finite positions remain. Vertex and skin influence statistics are properties of that same definition.

Indexed primitives contribute each base POSITION accessor element once, regardless of how many times the index stream references it. Unindexed primitives contribute each base position-stream element. These are the same counting semantics as vertex_count. In text output, the value appears as geometry centroid (x, y, z) beside the geometry bounding-box size, or as geometry centroid unavailable when no finite position exists. JSON carries the optional three-number geometry_centroid array.

Each mesh definition has additional_influence_sets, an always-present array of authored secondary glTF skin-attribute sets discovered across its primitives. Each entry has a numeric set_index of at least 1 plus independent joints_present and weights_present booleans. The accompanying joints_without_weights_present and weights_without_joints_present booleans preserve unpaired declarations on individual primitives, even when the mesh also has the complementary side on another primitive. Entries are strictly ascending by set_index and appear at most once. The array is empty when no secondary set was authored. It excludes set 0: max_joints_per_vertex and the weight-sum extrema retain their paired primary JOINTS_0 / WEIGHTS_0 semantics and do not incorporate additional sets.

This is source-presence evidence, not secondary-skinning evaluation. animsmith does not decode the additional per-vertex values into the core skinning model, include them in weight statistics, repair unpaired sets, or preserve their payloads when writing a converted asset. Keep the raw source when this evidence matters; a consuming pipeline decides whether a non-empty or unpaired set is acceptable.

Material and image evidence is deliberately separate from mesh definitions. material_resource_coverage is "complete" for glTF/GLB input and "unavailable" when the loader cannot provide the source-resource sidecar. For glTF/GLB, complete means the loader inspected the entire documented core source-resource domain: material, texture, and image definitions plus exactly the base_color, normal, metallic_roughness, occlusion, and emissive material-texture slots. It does not claim that extension-defined texture slots or every glTF material feature are modeled.

When coverage is complete, material_definitions, textures, and images are source-indexed records in ascending source order. A material definition has its optional display name and zero or more bindings { "slot", "texture_index" }. Bindings use the five-slot order listed above; an emissive-only material therefore has one emissive binding rather than an empty list. A texture record has texture_index, optional name, and its image_index. This preserves shared images and textures without duplicating metadata per material slot.

An image record has image_index, optional name, a source_kind of "embedded", "data_uri", or "external", and optional declared and decoded metadata. declared_mime_type is the source's declared MIME type; it is source-authored text, not proof of the payload. detected_container is the byte-detected png or jpeg container. decoded_color_type is one of l8, la8, rgb8, rgba8, l16, la16, rgb16, or rgba16; its channel_count is respectively 1, 2, 3, or 4, while width and height are decoded pixel dimensions. Thus MIME describes a declared media label, container describes encoded bytes, and color type/channel count describe the decoded pixel representation. These facts can differ without contradiction. Available images always have decoded metadata and no unavailable_reason. An unavailable image has a stable unavailable_reason and no decoded dimensions, color type, or channel count; a recognizable corrupt PNG/JPEG may still report its detected container. Reasons are source_unavailable, invalid_data_uri, unsupported_container, decode_failed, and resource_limit. This is inventory evidence, not an image acceptance decision: animsmith does not repair, resize, transcode, or judge color-space, normal-map, engine-import, or artistic suitability here.

The records describe what the loader observed, not authority for later writes or conversion recipes. Complete coverage does not promise writer or convert preservation of any observed binding or image payload, and a material-texture recipe remains a separate explicit conversion input. It also makes no image acceptance, repair, resize, transcode, color-space, or engine-import decision. Preserve the raw source if those source details must be retained.

node_instances contains one record per mesh-bearing node. node_index and mesh_index are stable source indices, so names need not be unique. Its static_node_world_aabb transforms the definition's finite positions by that node's default/rest world transform before reducing. It includes rotation and negative or non-uniform scale, but explicitly excludes animation, skin deformation, morph deformation, and runtime world placement. A missing box is explained by static_node_world_aabb_unavailable_reason: no finite positions, excluded skinned deformation, or a non-finite effective transform. The producer must emit that reason exactly when the corresponding static box is unavailable.

scenes contains every declared source scene. Each record counts its reachable mesh-bearing nodes in instance_count and unions their available static node-instance boxes in static_scene_world_aabb; excluded_instance_count makes partial coverage explicit. A node can contribute to more than one scene. Mesh-bearing nodes not reachable from a declared scene remain node instances but contribute to no scene aggregate. default_scene_index is present only when the source names a default scene; its absence does not select scene zero.

skeleton_source_coverage separates a loader that cannot expose source-node and source-skin identity from a source that genuinely contains no nodes or skins. When it is "unavailable", both skeleton_nodes and skins are empty. When it is "complete", skeleton_nodes is the source node table in ascending node_index order. node_index, parent_node_index, skin index, and joint index are source identities; names are display data and need not be unique. scene_root_indices lists the source scenes that directly declare the node as a root, in ascending source-scene order. A joint can name any source node as its parent, including a non-joint helper node. Structurally inconsistent source identity tables, such as a missing parent or parent cycle, downgrade the whole skeleton source domain to "unavailable" instead of publishing a self-contradictory complete table.

Every source node records its source-projected local_rest, tagged as "trs", "matrix", or "unavailable". For glTF this is the exact authored node member. FBX instead reports ufbx's documented metre/Y-up, inheritance-compensated projection, which can include adjusted transforms and generated helper nodes; it is not the raw FBX transform stack. A TRS has translation_parent_space_m, rotation_xyzw, and scale; the quaternion is [x, y, z, w]. The translation is expressed in the direct parent's coordinate frame. Even though glTF linear distances use metres, its numeric value is not directly comparable with a mesh-local or scene-world AABB until ancestor transforms are composed. A matrix is a 16-element column-major local transform. Measurements v10 and earlier called the TRS field translation_m; v11 renamed it so adapters cannot accidentally erase the coordinate domain.

rest_world_matrix is the separately derived, column-major default/rest world matrix. rest_world_translation_m repeats indices 12, 13, and 14 of that matrix as a directly consumable world-domain translation. rest_world_linear describes its upper-left 3x3 matrix with X/Y/Z column lengths, determinant, orientation sign, an optional canonical rotation_xyzw for the proper orthogonal polar factor of a unit-classified linear matrix, an optional common orthogonal uniform_scale, and one stable classification: unit_orthonormal, uniform_scaled, non_uniform, sheared, reflected, singular, or non_finite. After singularity, reflection takes classification precedence over shear and scale shape; axis lengths and the negative orientation retain the remaining evidence. Measurements v12 derives its finite f64 axis, determinant, and cross-axis facts through the shared affine facts, then applies its tolerant measurement policy: every length is compared to the three-axis mean using the longer operand, orthogonality is normalized by each axis pair, and singular/reflected/sheared/unit-or-uniform/non-uniform precedence remains measurement-specific. This reconciles the 1.0, 1.0, 1.000012 fixture with shared uniform-affine facts and gives the same observation after an axis permutation. v11 remains the immutable historical contract with its former derivation. Relative 1e-5 orthogonality/equal-axis tolerances and a scale-relative 1e-6 determinant tolerance make the result independent of a uniform choice of source units. The derived numeric facts are calculated and serialized with f64 precision so scale-cubed determinants do not overflow or underflow across the finite f32 source-matrix range. A non-finite or transitively unavailable world matrix carries its typed unavailable reason and a non_finite linear classification without fabricated numeric fields.

The local representation is never silently decomposed or replaced by the world transform. A declared scene is membership only, not a transform domain: scene_root_indices is membership evidence and adds no transform. A transformed node selected as a scene root retains its own projected local-rest record, and its ancestors determine the derived world matrix. These composition rules follow the glTF node hierarchy and transform definition.

skins is a source-skin table in ascending skin_index order. Its joints are in declared skin-slot order; each joint row owns its joint_index, node_index, joint_bind_to_mesh, and mesh_bind_world observations. skeleton_root_node_index is present only when the source explicitly declares one. The wire field inverse_bind_accessor reports whether the source inverse-bind declaration was absent, readable, empty, count-mismatched, or unreadable. A readable finite declaration retains its column-major matrices in slot order. For glTF these are exact accessor values. For FBX they are matrices projected from ufbx's converted cluster binds, not raw FBX payloads. Absent, malformed, and non-finite inverse-bind evidence is not inferred from node-local rest data.

Each skin's attachments names every source node that declares use of that skin, in source-node order. In each joint row, joint_bind_to_mesh is inverse(inverse_bind_matrix), so it maps joint bind-local coordinates into the mesh-local bind domain declared by that skin; mesh_bind_world is joint_rest_world * inverse_bind_matrix, mapping that mesh-local bind domain into world bind coordinates when the projected rest and bind evidence agree. These remain per-joint observations rather than claims that rows agree. Attachments are identity evidence, not an extra transform folded into either calculation. Each available derived field preserves its finite matrix and adds the same linear facts used by rest-world nodes. Otherwise it carries a typed unavailable reason. Both observations repeat the exact finite source_inverse_bind_matrix for their retained declaration slot, even when a later derivation is unavailable, so a consumer can verify the arithmetic without a second source decoder. For glTF that matrix is the exact finite accessor value; for FBX it is the finite matrix derived from ufbx's converted cluster binds and is explicitly not a raw-payload preservation claim. joint_bind_to_mesh.inversion_quality reports 1 / (norm_inf(A) * norm_inf(inverse(A))) for the source linear 3x3. This scale-free value is zero for a singular source and approaches zero as forward error amplification grows. Values at or below 1e-6 refuse as inverse_bind_matrix_ill_conditioned; an exactly singular linear part uses inverse_bind_matrix_non_invertible. A source bottom row must be affine within absolute 1e-6 of [0, 0, 0, 1], which admits ordinary binary32 round-trip noise while refusing projective matrices as inverse_bind_matrix_non_affine. mesh_bind_world has no inversion quality because it multiplies rather than inverts the retained source-declaration matrix. joint_bind_linear_summary summarizes only joint_bind_to_mesh: it reports joint/available/unavailable counts and distinguishes a shared consistent_uniform factor, differing mixed_uniform factors, non_uniform_or_sheared, reflected_or_singular, mixed groups, and partial or total unavailability. The detailed joint rows remain authoritative. These are descriptive calculations only. animsmith does not decide whether a consumer requires a joint, whether a skin/rest comparison is close enough, which root is canonical, or whether an unavailable matrix is acceptable.

For example, an exporter may put a 0.01 scale on an ancestor and author a child socket translation of 11.5. The local value remains 11.5 parent-space metres while the accumulated rest-world contribution is approximately 0.115 m. A skin can still render at its intended size when an inverse bind contributes the compensating factor: a joint may show effective rest-world scale 0.01, retained inverse-bind scale 100, joint-bind-to-mesh scale 0.01, and mesh-bind-world scale 1. That compensation applies to the skinning equation; an ordinary weapon, effect, or collision object parented to the socket generally inherits the socket's non-unit affine scale. The measurements expose all three domains so a consumer can apply its own attachment policy. They do not infer authored units from character height, mesh size, or any other plausibility heuristic. The matrix relationship follows the glTF skinning definition.

These facts help an artist or engine integration diagnose common handoff questions without reparsing the source: whether a joint is nested below a helper, whether a transformed scene root participates, whether two mesh nodes reuse a skin, and whether inverse-bind data is missing or singular. The node and skin terminology follows the glTF 2.0 specification. They do not select a required bone list, validate an engine's tolerance, retarget, normalize, repair, or rewrite the asset; those remain consuming project policy.

Lint adds exactly one files[].checks[] record for every built-in catalog check. Each record keeps these dimensions independent:

  • selection: selected or unselected;
  • configuration: enabled or disabled; a check is disabled when its severity is off or its built-in policy is opt-in and no enabling severity was configured;
  • applicability: applicable or not_applicable;
  • evaluation: complete, partial, or not_evaluated;
  • content findings;
  • completed evaluated_scopes and typed coverage gaps.

Gap and scope code fields are the machine contract; message is display text and must never be parsed. Disabled, unselected, and not-applicable checks are not artificial gaps. A partial check has at least one completed scope and at least one gap. Applicable work that completed nothing has a gap and no content findings. A scope can appear as completed and also be named by a gap when a group-level calculation covered some but not all members.

Built-in gap codes are:

Gap codeMeaningEmitted by
roles_unresolvedRequired semantic rig roles were not resolved.loop-seam, root-motion-speed, in-place, foot-slide, gait-group, time-complement
measurement_unavailableA required numeric measurement could not be produced or did not meet its evidence floor.loop-closure, duplicate-loop-endpoint, loop-seam, loop-seam-vel, loop-seam-rot, root-motion-speed, in-place, foot-slide, gait-group, sync-group, time-complement, rest-world-scale
skeleton_unavailableRequired skeleton presence work could not run because the file has no usable skeleton.required-bones
node_selector_no_matchA configured source-node selector matched no named source node.rest-world-scale
node_selector_ambiguousA configured source-node selector matched more than one named source node.rest-world-scale
insufficient_measurable_membersFewer than two configured group members produced usable comparison evidence.gait-group, sync-group, time-complement
members_not_evaluatedSome configured group members did not produce usable comparison evidence.gait-group, sync-group, time-complement
invalid_declared_fpsA declared frame rate was zero, negative, or non-finite.fps
sync_frame_grid_unavailableA same-time sync-group member lacks usable declared frame-grid evidence.sync-group
insufficient_rotation_evidenceToo few usable rotation tracks existed for a bind-pose comparison.bind-pose

Built-in completed/gap scope codes are:

Scope codeWork unitEmitted by
loop_closureOne named clip's per-bone model-space pose closure was measured.loop-closure
duplicate_loop_endpointOne named clip's authored tracks were analyzed for redundant closing endpoint keys.duplicate-loop-endpoint
member_existenceConfigured group members were checked for existence.gait-group, sync-group, time-complement
phase_measurementOne named clip's gait phase was measured or lacked usable evidence.gait-group, time-complement
phase_coherenceOne named group's measurable gait phases were compared.gait-group, time-complement
sync_member_measurementOne named same-time sync-group member's timing evidence was measured.sync-group
sync_compatibilityOne named same-time sync group had compatible member timing evidence compared.sync-group
loop_seamOne named clip's positional loop seam was measured.loop-seam
loop_seam_velocityOne named clip's per-bone model-space seam velocity continuity was measured.loop-seam-vel
loop_seam_rotationOne named clip's per-bone model-space angular seam velocity continuity was measured.loop-seam-rot
required_bone_presenceConfigured structural skeleton-bone presence requirements were evaluated.required-bones
selected_node_rest_scaleOne configured source-node selector resolved and its effective rest-world linear scale was evaluated.rest-world-scale
root_motion_speedOne named clip's root-motion speed was measured.root-motion-speed
travel_modeOne named clip's XZ movement-owner declaration was judged.in-place
foot_stanceWhole-clip prerequisites for stance analysis were evaluated.foot-slide
left_foot_stanceThe named clip's left foot/toe stance was evaluated.foot-slide
right_foot_stanceThe named clip's right foot/toe stance was evaluated.foot-slide
frame_gridThe named clip's declared frame grid was evaluated.fps
first_frame_rest_deltaThe named clip's first-frame/rest-pose rotation evidence was evaluated.bind-pose
animation_asset_labelOne source animation index was projected to the selected engine profile's canonical asset-label selector.engine-addressability
animation_asset_label_inventoryComplete source-animation inventory required for asset-label prediction was unavailable.engine-addressability
scene_asset_labelOne source scene index was projected to the selected engine profile's canonical asset-label selector.engine-addressability
default_scene_routeThe source default-scene observation was projected to the selected engine profile's route to an existing scene asset.engine-addressability
skin_asset_labelOne source skin index was projected to the selected engine profile's conditional canonical skin asset-label selector.engine-addressability
inverse_bind_matrices_asset_labelOne source skin index was projected to the selected engine profile's canonical inverse-bind-matrices asset-label selector.engine-addressability
named_addressability_mapOne selected engine profile named-addressability map and its duplicate-name policy were evaluated.engine-addressability
animation_target_idOne unique source animation target node's exact path and target identifier were evaluated.engine-addressability
gltf_addressability_inventoryComplete raw glTF scene, node, skin, attachment, and path evidence required for rich addressability prediction was unavailable.engine-addressability
engine_clip_boundaryOne source animation clip's exact end-frame boundary was evaluated.engine-clip-boundary
engine_clip_boundary_inventoryComplete exact source-animation boundary inventory was unavailable.engine-clip-boundary

The built-in gap and scope declarations in animsmith_core are authoritative for each code's identity, meaning, and allowed emitting check ids. Runtime evaluation rejects a built-in code from an undeclared emitter, and the output contract test derives this reference inventory from those same declarations. The public code slices let consumers enumerate or allow-list animsmith's built-in vocabulary; the meaning/emitter registry remains an implementation detail. Custom checks may add namespaced gap codes and their own namespaced scope vocabulary.

summary.checks reports a total and four independent partitions. Each of selection, configuration, applicability, and evaluation sums to that same total. summary.checks.gaps counts typed gaps, while summary.findings counts content findings by severity.

Engine-prediction provenance and scoped facets

Every output-v19 lint file has required nullable prediction_provenance. It is null when no exact engine profile was resolved. Revision-1 profiles carry immutable prediction-provenance v3 (urn:animsmith:prediction-provenance:3): the typed profile facts and sources, authoritative input format, bounded resolved document/per-clip settings, V2 raw-source facts including optional exact source-timing observations, the same-load dependency closure, and consumed contract identities. The header, profile, settings coverage/work, raw evidence, closure, and primary input identities are cross-validated; host paths and arbitrary JSON are forbidden.

Output v10 also carries the first bounded production rule. For the exact Bevy revision 1 / 0.19.0 / gltf-asset-loader profile on glTF or GLB, engine-addressability predicts the canonical GltfAssetLabel::Animation(source_clip_index) display selector. Each available animation_asset_label facet uses the exact Animation{index} selector as its scope subject. Names do not control the selector, and the index may change when the source animation order changes. Complete empty source-animation inventory is not applicable. Partial or unavailable inventory emits one subjectless animation_asset_label_inventory required-unavailable facet and no prefix predictions.

Output v14 introduced the bounded engine-clip-boundary rule for the exact Unreal revision 1 / 5.8 / fbx-importer profile. Its FBX adapter supplies the parser-resolved absolute animation-stack end coordinate and exact frame period as generic exact source-timing evidence. The rule reports only whether that end coordinate lies on the exact frame lattice. Missing, partial, or unavailable evidence becomes a required-unavailable facet; it does not infer frame ranges, resampling, root motion, or other importer behavior.

Output v15 adds engine-unit-scale for the exact Bevy revision 2 / 0.19.0 / gltf-asset-loader profile on glTF JSON or GLB. V4 provenance binds exact profile facts, fully materialized settings with value origins, the complete alias-normalized runtime-node selector declaration, raw source and dependency evidence, and a bounded same-load scene/attachment/primitive inventory. Available facets carry typed machine results for exact unit mapping, loader-created scene entities, primitive children, and selected source nodes. The caller-owned WorldAssetRoot and arbitrary application world-unit policy are outside the result. Unsupported matrix ancestry, selector miss/ambiguity, incomplete evidence, and exhausted facet work remain typed, unsuppressible required-unavailable states; this prediction-only rule emits no content findings and authorizes no rescaling.

The current successor adds the narrow Bevy revision-3 engine-track-support prediction through V5 provenance and the output-v19 contract. Its same-load raw animation inventory is bounded by both animation coverage and independent per-animation channel coverage. With a complete inventory, the two gate settings produce only negative outcomes for dropped animation/channel rows; a dropped row is not a content finding. The compiled feature gate takes precedence over load_animations. When both gates allow loading, the result is a stable required-unavailable runtime-survival facet, because this contract does not execute Bevy or claim positive runtime survival.

Complete-empty animation inventory is not applicable. Partial or unavailable inventory emits exactly one subjectless, unsuppressible inventory required_prediction_unavailable facet and no retained-prefix prediction; bounded N+1 work is therefore distinguishable from a complete inventory. The ordinary lifecycle remains explicit: all available work is complete, a mix of available and unavailable facets is partial, and all required- unavailable work is not_evaluated. V5 readback validates result state, facet scope, inventory-row basis, and provenance identity under the same bounds before accepting an available result; malformed or contradictory serialized claims are rejected rather than repaired into a prediction. Extensions, unsupported animation constructs, and other positive runtime claims remain outside this revision-3 slice. Output-v17, output-v16, and output-v15, V4 provenance, and revision-2 behavior remain preserved and readable; output-v19 carries the immutable V5 provenance for the Bevy slice, while Unity Generic root-motion uses V6 provenance.

For the exact Unity Generic revision-2 / 6000.3 / fbx-model-importer tuple, the engine-root-motion check is serialized in output-v19 with immutable urn:animsmith:prediction-provenance:6 and urn:animsmith:engine-prediction:6 identities. V6 extends the same-load V5 evidence with the raw FBX transform-path inventory and the normalized per-clip movement-owner intent. Each declared clip/axis facet records a RootMotionRouting result: gameplay must pair with baked_into_pose, and animation must pair with stored_as_root_motion. Conflicts are ordinary error findings bound to the available facet's prediction_scope.

The prediction lifecycle is independent of the ordinary measurement checks. An all-available run is complete; mixed available and unavailable facets are partial; all required-unavailable facets are not_evaluated. Missing or ambiguous source-path resolution, a path that does not identify the explicitly resolved Root role, incomplete raw-path or project-intent coverage, settings overflow, duplicate clip names, or unavailable translation/yaw evidence yields required_prediction_unavailable. Such a facet is not a content finding and cannot be suppressed by --allow; it makes lint exit 1. A missing Root never falls back to Hips for this rule. The consumer-neutral measurement layer may still use Hips as its historical fallback, but root-motion prediction requires the resolved Root source role.

Project-intent scans retain at most each bounded source/declaration prefix plus one overflow witness. An ownerless unmapped tail is represented as NPlusOne intent work and yields one atomic unavailable summary; only complete evidence that no owner is declared preserves the check's not-applicable result. Raw path evidence is required once declared work exists.

The numeric root trajectory is evidence, not a routing threshold. Translation and yaw availability must be measured for the corresponding axis; zero travel, nonzero travel, and any other finite magnitude follow the same owner-versus-setting comparison. This check performs no Unity editor execution, imported-asset readback, runtime playback, or engine certification.

Raw FBX paths use a closed, case-sensitive, byte-exact grammar: / is the only separator and there is no escaping or Unicode normalization; segments are nonempty, with no leading/trailing or doubled separators, ./.., backslashes, controls, or Unicode format characters. Segment/path/depth limits are 1,024 UTF-8 bytes, 4,096 bytes, and 256 segments. The same input bytes are projected through a raw-preserving ufbx load so source identities, parent chains, and names remain auditable. The implicit ufbx root and generated geometry/scale helpers are retained as evidence but cannot match. Only complete inventory coverage makes zero matches a proven NoMatch; partial or unavailable coverage is CoverageIncomplete.

Resolved engine settings and prediction provenance v3 use explicit bounded N+1 work and coverage evidence. A 4,097th clip produces partial settings coverage rather than an honest complete prefix; engine facets report the typed resolved_settings_overflow reason. Current lint allocates the shared 4,096 facet budget before evaluation and emits one canonical rule-scoped facet_budget_exceeded summary when candidates are omitted.

This is selector evidence only: it does not establish that Bevy loaded the input, that animation loading was enabled, that an asset exists at runtime, or that targets and graph wiring survived. Other exact profiles currently retain nonnull provenance with no production prediction. Engine-backed checks attach one prediction object to their existing check record. Its canonical facets are keyed by the existing EvaluationScope so one check can retain an available result for one clip or selector and a required-unavailable result for another.

A facet state is exactly available or required_prediction_unavailable. Available facets have nonempty typed basis and no reasons. Required-unavailable facets keep any available basis prefix and one or more stable reason codes; they are not content findings or generic coverage gaps. Available scopes occur in evaluated_scopes; unavailable scopes occur in neither completed scopes nor gaps. Consequently an all-available check is complete, mixed available/unavailable work is partial, and all-unavailable work is not_evaluated under the existing single evaluation lifecycle.

Findings on a prediction-bearing check include prediction_scope and must bind to exactly one available facet. Severity and --allow may filter or block a content finding but never suppress required-unavailable prediction work. summary.prediction_facets contains explicit available and required_prediction_unavailable counts; any nonzero unavailable count makes lint exit 1.

Basis rows are closed typed references to embedded profile facts, resolved settings, project/config fields, raw-source facts, measurements-v16 scalars, or primary sources. Measurement references use bounded canonical JSON pointers and exact scalar values. A schema-valid measurement availability of unavailable may make prediction work unavailable. A malformed or non-finite present measurement remains a contract error and exit 2.

The report reader caps each serialized input at 256 MiB before UTF-8 or JSON parsing. It validates provenance and measurement-independent prediction links, then the complete version-matched measurements contract, then measurement-pointer values, before diff can extract measurements. Output v10 and earlier inputs receive the normal regeneration guidance.

lint --format json deliberately rejects --allow so machine evidence is never deleted. --allow remains available for text and Markdown presentation and their exit policy. Text and Markdown render coverage gaps separately from findings and group repeated gaps by (check_id, code) for readability. Group counts still reflect every underlying per-scope JSON gap.

Findings and numeric values

Findings carry check_id, severity, optional clip, bone, node, time_s, measured, and expected fields, plus a human message. node is a source-node path whose components include stable source indices; it is distinct from the normalized skeletal bone context used by animation checks. Treat check_id and the structured fields as automation data; treat message as display text. Group-level findings may also carry members, a configured-order array whose rows contain the member name and a key-sorted map of scalar measurements. Missing or unavailable evidence stays explicit in those rows; consumers do not need to parse the finding message to recover the comparison table. The nested check_id intentionally repeats its owning check record so a finding stays self-describing when extracted or consumed through the embedded API; the evaluator rejects mismatched parent/child ids. For loop-closure and loop-seam-vel, expected is the effective cap for that finding's clip after exact-name and glob expectations are resolved, with the corresponding global check setting or built-in default as fallback.

Numeric equality in the JSON contract means equality of decoded JSON numbers, not byte-for-byte lexical spelling. For example, 1, 1.0, and 1e0 denote the same numeric value to a conforming adapter.

diff

diff --format json uses the current output-v19 header and emits inputs, a delta count, and structured metric deltas:

{
  "schema_version": 19,
  "schema": "urn:animsmith:schema:output:19",
  "tool": {
    "name": "animsmith",
    "version": "0.14.0",
    "source": { "revision": null, "dirty": null }
  },
  "command": "diff",
  "inputs": { "before": "old.glb", "after": "new.glb" },
  "summary": { "deltas": 1 },
  "deltas": [
    { "clip": "walk", "metric": "speed_mps", "before": 1.0, "after": 1.2, "note": "moved" }
  ]
}

diff accepts asset files, current output-v19 with measurements-v18, or historical output-v17, output-v16, output-v15, output-v14, or output-v13 measure/lint reports with measurements-v16, and version-matched historical output-v11/v12 reports with measurements-v15. The output-v9 contract and its measurements-v9 pairing remain immutable historical evidence; output-v10 and earlier reports are rejected with guidance to regenerate them from the original asset. Multi-file reports and other unsupported contract versions are also rejected as operator errors. Before extracting the clip metrics it uses, diff validates the complete measurement record, including mesh evidence, and rejects malformed or non-finite payload values.

Loop-continuity rows compare by bone_index; availability changes are reported at loop_continuity.bones[N].availability. Numeric fields are compared only when present on each side. Re-export noise at or below 0.001 m for position_delta_m, 0.1 degree for rotation_delta_deg, and 0.01 m/s for seam_velocity_delta_mps is silent; the 0.5 degree/s floor applies to seam_angular_velocity_delta_degps. Larger changes produce metric paths such as loop_continuity.bones[12].rotation_delta_deg. These are diff significance floors, not the lint caps configured under [checks.loop-closure], [checks.loop-seam-vel], and [checks.loop-seam-rot].

Channel-coverage membership compares the canonical (bone_index, property) set, so a display-name-only change is not reported as lost/gained coverage. Every root-trajectory status, selected-bone/source identity, and heading-axis change is exact. Each translation field uses a 0.001 m significance floor; each yaw field uses 0.1 degree, with circular 360-degree distance only for net_yaw_deg. Unwrapped yaw and yaw travel compare ordinarily, so 0 -> 360 remains visible even when net yaw is unchanged.

Pack evaluations

  • Commercial-pack evaluation guide — Read the maintained Mixamo and Protofactor technical-report/evidence pairs and separate their scoped findings from project acceptance.
  • Animation-pack evaluation reports — Review evidence-backed marketplace-pack assessments and the reporting convention for constituent packs and collection-level cross-pack summaries.

Commercial-pack evaluation guide

The maintained animation-pack evaluation reports index the current Mixamo and Protofactor technical-report/evidence-appendix pairs. They are scoped technical snapshots of authorized inputs, not redistribution advice, a license opinion, or approval for a different target game.

Who these reports are for

The pairs exist to prove the tool on real deliveries: every claim in them comes from running AnimSmith over a pack as a developer actually receives it. A developer deciding whether to buy a pack should read the scorecard first, which gives every maintained pack its verdict, completeness, and confidence in one row, and then read the Technical decision and Technical issue register of the single pair that matters. The reports assess delivered files, not a purchase: no per-pack buyer's note exists, and none is implied.

Read a pair in order

  1. Read the Technical decision for the scoped verdict and its conditions.
  2. Read Capability coverage and Runtime sets and authored motion to learn which logical clips, blend/transition/mask/contact sets, and gaps were actually evaluated.
  3. Read Engine status as bounded evidence for the named engine procedure; it is not a transfer of proof to another engine, version, character, graph, or project.
  4. Read the Technical issue register in the selected technical report to identify whether a source/vendor, DCC, engine, gameplay, or visual owner must act.
  5. Open the paired Detailed evidence appendix for input identity, evaluator/version, commands, digests, coverage, and repeatable procedure.

The report index mechanically links every maintained report to its appendix. Use the pair rather than an isolated headline when deciding whether a pack is worth an engine trial.

What to watch for

QuestionWhere to read itWhat it does not establish
Is the delivery technically useful for the named scope?Technical decision and fit/limitationsA general “game-ready” certification.
Which clips and runtime sets have evidence?Capability coverage and runtime setsThat undeclared, missing, or untested members behave the same way.
What needs work, and who owns it?Technical issue register and evidence appendixThat a tool can automatically retarget, author a graph, or make an artistic choice.
Which engine behavior was observed?Engine status and appendix procedureImport, playback, blend, mask/contact, or visual success in another project.
Why might current and retained evidence differ?Changes between AnimSmith versionsA current workflow rule or a new verdict by itself.

Keep evaluator-change explanations in each report's Changes between AnimSmith versions section. The workflow above is intentionally current-state routing: for a new game, use the game-developer intake workflow to establish its own engine-observed and visual gates.

Workflow pages contain only current reader steps. Tracker chronology and internal rationale are excluded; evaluator changes stay in each report's Changes between AnimSmith versions section.

Commercial files, excerpts, motion-bearing derivatives, and generated engine projects stay outside the repository and CI. The checked-in reports preserve scrubbed, reproducible evidence conventions while the source remains in its authorized location.

Animation-pack evaluation reports

These reports apply the repository's versioned animation-pack evaluation skill to delivered game-animation assets. Each report is a dated technical snapshot, not a vendor guarantee, license opinion, or substitute for testing in the target game.

Scorecard

Every cell after the pack name is copied from that report's header block, so the scorecard reaches no conclusion of its own and carries no numeric score. It is the fastest way to pick a pair; read the pair itself for the reasoning. The completeness column carries only the word — each report states the evidence boundary behind it — and Current reports carries the scope and evaluation status of every pair.

PackTechnical verdictEvaluation completenessConfidenceEvaluation dateCurrent evaluator
Protofactor Basic LocomotionUsable with conditionspartialmedium2026-09-01AnimSmith 0.10.0
Protofactor Sword & ShieldUsable with conditionspartialmedium2026-09-01AnimSmith 0.10.0
Protofactor CampfireUsable with conditionspartialmedium2026-09-01AnimSmith 0.10.0
Protofactor ClimbingUsable with conditionspartialmedium2026-09-01AnimSmith 0.10.0
Protofactor InjuredUsable with conditionspartialmedium2026-09-01AnimSmith 0.10.0
Protofactor 1-Handed MeleeUsable with conditionspartialmedium2026-09-01AnimSmith 0.10.0
Protofactor 2-Handed MeleeUsable with conditionspartialmedium2026-09-01AnimSmith 0.10.0
Protofactor Dual SwordsUsable with conditionspartialmedium2026-09-01AnimSmith 0.10.0
Protofactor Ultimate Animation CollectionInsufficient technical evidencepartialmedium2026-09-01AnimSmith 0.10.0
Mixamo Basic LocomotionInsufficient technical evidencepartialmedium2026-09-01AnimSmith 0.10.0
Mixamo Female Basic LocomotionInsufficient technical evidencepartialmedium2026-09-01AnimSmith 0.10.0
Mixamo Female LocomotionInsufficient technical evidencepartialmedium2026-09-01AnimSmith 0.10.0
Mixamo LocomotionInsufficient technical evidencepartialmedium2026-09-01AnimSmith 0.10.0
Mixamo Longbow LocomotionInsufficient technical evidencepartialmedium2026-09-01AnimSmith 0.10.0
Mixamo Magic LocomotionInsufficient technical evidencepartialmedium2026-09-01AnimSmith 0.10.0
Mixamo Male LocomotionInsufficient technical evidencepartialmedium2026-09-01AnimSmith 0.10.0
Mixamo Pistol-Handgun LocomotionInsufficient technical evidencepartialmedium2026-09-01AnimSmith 0.10.0
Mixamo Rifle 8-Way LocomotionInsufficient technical evidencepartialmedium2026-09-01AnimSmith 0.10.0
Mixamo Locomotion CollectionInsufficient technical evidencepartialmedium2026-09-01AnimSmith 0.10.0

Report organization

Keep one linked report pair per constituent animation pack:

  • a concise technical report for the decision, capability tiers, important runtime sets, integration recipe, issue ownership, engine status, and fit;
  • an evidence appendix for source identity, canonical roles, pipeline stages, readiness evidence, validation profiles, commands, digests, and detailed engine/compatibility procedures.

This keeps the reader-facing decision short and reevaluation bounded when one pack, engine, or evaluator version changes.

The current linked-pair layout is report format version 2. Both documents name one current AnimSmith evaluator and present ordinary sections as current state for a game developer. Prior evaluator behavior, superseded evidence, and version comparisons belong only in Changes between AnimSmith versions. Report-format version 1 remains an immutable historical/generated contract.

Add a collection-level report pair after multiple constituent packs have been evaluated, even while the rollup is partial. Name evaluated and missing constituents. Its concise report should link rather than duplicate the constituent reports and own only collection-wide conclusions:

  • evaluated and missing constituent packs;
  • combined gameplay/content coverage and meaningful duplicates;
  • cross-pack skeleton, scale, root-motion, timing, style, and retarget paths;
  • cross-pack blend, transition, mask, interaction, and motion-database sets;
  • gaps that one constituent pack fills for another;
  • collection-level value, adoption conditions, and confidence boundaries.

Build the rollup manifest by namespacing validated constituent manifests. Digest-compare every overlapping logical package path before claiming safe co-installation. Treat skeleton identity, engine graph execution, visual blend quality, masking/contact behavior, and target-character acceptance as separate claims. For unarmed/armed combinations, use a full-body state handoff as the default; promote masks only with member-specific pelvis/root/contact evidence.

Reference cross-pack runtime-set members with stable namespaced motion ids such as protofactor-basic-locomotion:walk-forward-unarmed. Never imply compatibility merely because two pack reports use the same canonical primary role. Cross-pack sets require their own grouping evidence and validation.

Use flat, stable filenames for each pair:

  • <vendor>-<pack>.md and <vendor>-<pack>-evidence.md for a constituent pack;
  • <vendor>-<collection>.md and <vendor>-<collection>-evidence.md for a collection rollup.

Current reports

Technical reportEvidence appendixScopeEvaluation status
Protofactor Basic LocomotionDetailed evidenceOne locally held Basic Locomotion pack from the Ultimate Animation CollectionPartial: current AnimSmith 0.10.0 mechanical evaluation and 39 unpromoted candidates; no current engine or visual acceptance
Protofactor Sword & ShieldDetailed evidenceOne locally held Sword & Shield pack from the Ultimate Animation CollectionPartial: current AnimSmith 0.10.0 mechanical evaluation and 28 unpromoted candidates; no current engine or visual acceptance
Protofactor CampfireDetailed evidenceOne locally held Campfire pack from the Ultimate Animation CollectionPartial: current AnimSmith 0.10.0 mechanical evaluation and one unpromoted candidate; no current engine, contact, or visual acceptance
Protofactor ClimbingDetailed evidenceOne locally held Climbing pack from the Ultimate Animation CollectionPartial: current AnimSmith 0.10.0 mechanical evaluation and one unpromoted candidate; no current engine, environment, or visual acceptance
Protofactor InjuredDetailed evidenceOne locally held Injured pack from the Ultimate Animation CollectionPartial: current AnimSmith 0.10.0 mechanical evaluation and 15 unpromoted candidates; no current engine, blend, or visual acceptance
Protofactor 1-Handed MeleeDetailed evidenceOne locally held 1-Handed Melee pack from the Ultimate Animation CollectionPartial: current AnimSmith 0.10.0 mechanical evaluation and 25 unpromoted candidates; no current engine, attachment, or visual acceptance
Protofactor 2-Handed MeleeDetailed evidenceOne locally held 2-Handed Melee pack from the Ultimate Animation CollectionPartial: current AnimSmith 0.10.0 mechanical evaluation and 25 unpromoted candidates; no current engine, attachment, or visual acceptance
Protofactor Dual SwordsDetailed evidenceOne locally held Dual Swords pack from the Ultimate Animation CollectionPartial: current AnimSmith 0.10.0 mechanical evaluation and 25 unpromoted candidates; no current engine, contact, or visual acceptance
Protofactor Ultimate Animation CollectionDetailed evidencePartial rollup of Basic Locomotion, Sword & Shield, Campfire, Climbing, Injured, 1-Handed Melee, 2-Handed Melee, and Dual SwordsPartial: current AnimSmith 0.10.0 mechanical rollup of 159 unpromoted candidates; no current cross-pack compatibility, engine, or visual acceptance
Mixamo Basic LocomotionDetailed evidenceOne archive-paired Basic Locomotion constituentPartial: exhaustive file/tool baseline and variant ownership contracts; licensing and runtime gates deferred
Mixamo Female Basic LocomotionDetailed evidenceOne archive-paired Female Basic Locomotion constituentPartial: exhaustive file/tool baseline and variant ownership contracts; licensing and runtime gates deferred
Mixamo Female LocomotionDetailed evidenceOne archive-paired Female Locomotion constituentPartial: exhaustive file/tool baseline and variant ownership contracts; licensing and runtime gates deferred
Mixamo LocomotionDetailed evidenceOne archive-paired Locomotion constituentPartial: exhaustive file/tool baseline and variant ownership contracts; licensing and runtime gates deferred
Mixamo Longbow LocomotionDetailed evidenceOne archive-paired Longbow Locomotion constituentPartial: exhaustive file/tool baseline and variant ownership contracts; licensing and runtime gates deferred
Mixamo Magic LocomotionDetailed evidenceOne archive-paired Magic Locomotion constituentPartial: exhaustive file/tool baseline and variant ownership contracts; licensing and runtime gates deferred
Mixamo Male LocomotionDetailed evidenceOne archive-paired Male Locomotion constituentPartial: exhaustive file/tool baseline and variant ownership contracts; licensing and runtime gates deferred
Mixamo Pistol-Handgun LocomotionDetailed evidenceOne archive-paired Pistol-Handgun Locomotion constituentPartial: exhaustive file/tool baseline and variant ownership contracts; licensing and runtime gates deferred
Mixamo Rifle 8-Way LocomotionDetailed evidenceOne archive-paired Rifle 8-Way Locomotion constituentPartial: exhaustive file/tool baseline and variant ownership contracts; licensing and runtime gates deferred
Mixamo Locomotion CollectionDetailed evidencePartial rollup of nine archive-paired locomotion constituentsPartial: mechanical rollup only; no cross-pack compatibility, engine, or visual acceptance claim

Commercial and other redistribution-restricted source files, excerpts, motion-bearing derivatives, and generated engine projects are not committed. They may be used only in an authorized external workspace for one-time local validation. Reports retain scrubbed artifact labels, non-recoverable digests, evaluator identity, coverage gaps, and procedures needed to repeat the work with authorized inputs. Published tool output about a licensed clip is bounded the same way, by the licensed-derivative rule.

Repository and CI validation uses only synthetic/self-authored fixtures or assets whose licenses explicitly permit repository inclusion and CI use/redistribution. CI must not fetch, cache, or publish a credential-gated marketplace pack.

Animation pack evaluation: Protofactor Basic Locomotion Animset

Technical verdict: Usable with conditions

Evaluation completeness: partial — current mechanical and declared-contract evidence is exhaustive, but no current engine or visual acceptance ran.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Protofactor Basic Locomotion evidence

Technical decision

The verified AnimSmith 0.10.0 release loads all 179 delivered FBXs. Untouched lint finds 24,186 constant-track notes and 36 time-monotonic errors across 12 files. Current declared contracts cover 177 individual motions; 58 command invocations pass and 119 fail per format, so loop and continuity results are conditions rather than approval. The declared remediation rerun generated all 39 candidates: 12 slices removed the 36 time-ordering errors, 24 in-place gait anchors reduced the three measured circular spreads from 0.7156245/0.4630161/0.6597812 to 0.0501911/0.0938395/0.0724415, and three constant-track candidates were emitted. These are mechanically verified, unpromoted candidates; no engine, visual, retarget, contact, or gameplay run occurred.

Capability coverage

Complete core

  • Delivered filename families cover locomotion, cover, turns, airborne actions, and transitions; 70 labelled root-motion files have matching in-place partners.

Partial supporting gameplay

  • Mechanical input health is measured. The current slices remove their known time-ordering errors, while remaining contract findings require loop and continuity decisions before locomotion blend adoption.

Absent

  • No current evidence establishes additive, first-person, paired interaction, engine acceptance, or artistic readiness.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=not-evaluated; declare selected locomotion rings rather than infer them from names.
  2. Timing/synchronization: sync=not-evaluated; resolve contract failures and measure selected loop members.
  3. State ownership: owner=not-evaluated; declare IP/RM movement ownership per clip.
  4. Composition constraints: composition=full-body; do not approve masks or additive use.
  5. Acceptance gate: gate=engine-and-visual-review; import, blend, retarget, and playtest selected candidates.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
BL-010majorTime ordering and loop continuity require declared review before runtime use.artist-authorReview the 12 external slice candidates and decide loop policy before promotion.Current declared slicing, gait anchoring, and constant-track pruning are mechanical candidates only; no automatic artistic repair is established.observed-animsmith; slices remove 36 time errors, while original contracts still have 119 failing invocations per format.

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unity unspecifiednot-evaluatedNo current import or playback run.Import, controller, visual, and build tests.
Unreal Engine unspecifiednot-evaluatedNo current import or playback run.Import, retarget, graph, and build tests.
Godot unspecifiednot-evaluatedNo current conversion, import, or playback run.Conversion/import, graph, and export tests.
Bevy unspecifiednot-evaluatedNo current glTF handoff or runtime run.Conversion, addressability, runtime, and performance tests.

Fit and limitations

Best fit is an engine project willing to declare and validate its locomotion contract. Cross-pack compatibility, visual loop quality, contacts, and artistic fit remain untested.

Changes between AnimSmith versions

AnimSmith 0.10.0 — official release revalidated the 179-FBX baseline and 177 declared contracts, then reran 12 declared slices, 24 in-place gait anchors, and three constant-track trials. Earlier 0.7.0 evaluator and any engine or offline evidence are historical only.

Evidence status

Current evidence uses the official 0.10.0 binary and the canonical readiness ladder. Commercial sources and derivatives remain external.

Sources

Animation pack evidence appendix: Protofactor Basic Locomotion Animset

Companion report: Protofactor Basic Locomotion report

Evidence status: partial — the official evaluator loaded the delivered FBXs and produced current baseline evidence.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

This appendix preserves current evidence only. The canonical readiness ladder remains authoritative.

Evaluation manifest schema: urn:animsmith:skill:animation-pack-evaluation-manifest:1.

Evaluation scope and provenance

FieldValue
Pack/editionProtofactor Basic Locomotion Animset; local constituent revision unknown
Vendor/sourceProtofactor Ultimate Animation Collection
Delivered scopeAuthorized local commercial delivery; 179 preserved FBXs
Target useEngine-neutral third-person locomotion intake
Target enginesUnity, Unreal Engine, Godot, and Bevy; not evaluated in this run
Target rigs/packsNo target character or current cross-pack run supplied
Source manifestExternal scrubbed inventory, SHA-256 c6cc4d541fa2cb8e4f3e14c283d5b925f83957db35bfa079309f250cdaf101ba
Evaluation manifesturn:animsmith:skill:animation-pack-evaluation-manifest:1; current binding unavailable because current output binding was not rendered for this retained report format
Acquisition/license provenanceCommercial source was authorized locally; no transaction or redistribution conclusion is made

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files179 FBXs179baseline completedmechanical and contract evidence captured
Rigs/export variantsUnknown00Not evaluated in this run
AnimSmith baseline179179current findings recordedinspect, JSON measure, JSON lint, and Markdown lint completed per file
Declared contracts177 motion-labelled files17758 pass; 119 fail per output formatMechanical contract results are current; semantic taxonomy, engine, and artistic acceptance were not evaluated
Offline visual reports000Not evaluated in this run
Engine import/playback4 runtimes00No engine import or playback run selected
Blend/mask/retargetUnknown00Measurements completed, but no target rig or blend/mask acceptance test ran

Claim legend

Current consequential claims use observed-file, observed-animsmith, documentation-stated, or not-evaluated as defined by the assessment taxonomy.

Evaluation manifest and taxonomy

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00Classification was not regenerated or accepted
continuous-locomotion00Classification was not regenerated or accepted
locomotion-transition00Classification was not regenerated or accepted
airborne00Classification was not regenerated or accepted
traversal00Classification was not regenerated or accepted
action-interaction00Classification was not regenerated or accepted
reaction-death00Classification was not regenerated or accepted
emote-cinematic00Classification was not regenerated or accepted
other-unknown00Classification was not regenerated or accepted
Total00179 delivered FBXs remain unclassified

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquirepartially-evaluatedAuthorized local bytes inventory only
Preserve rawevaluated-cleanRaw commercial source was not modified
Inspectevaluated-finding179 baseline attempts completed
Segmentnot-evaluatedNo separate segmentation trial selected
Root motionnot-evaluatedMeasurements completed; no root-motion policy or controller acceptance was evaluated
Conformpartially-evaluatedTwelve declared slices and 24 declared in-place gait anchors generated external candidates; no target-rig or engine acceptance was run
Validateevaluated-findingDefault lint completed for every FBX
Optimizepartially-evaluatedThree external constant-track candidates were generated; source was unchanged and runtime equivalence remains unproved
Exportpartially-evaluatedCandidates were written as external GLBs only; no engine handoff was selected
Gate/reportpartially-evaluatedCurrent baseline, remediation, and boundary recorded

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
Entire delivered corpusevaluated-findingnot-evaluatednot-evaluated

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — evaluator-selected-generic-scenarioSource inventory and current baseline captured
Blended locomotionselected — evaluator-selected-generic-scenarioMeasurements completed; selected IP/RM ownership, blend policy, and engine acceptance remain
Root-motion controllerselected — evaluator-selected-generic-scenarioMeasurements completed; controller trajectory policy and engine acceptance remain
State-machine transitionsselected — evaluator-selected-generic-scenarioRequires contract-qualified clip selection and engine test
Layered upper body/weaponsnot-selectedNo current action scope
Traversal/environmentnot-selectedNo current action scope
Contact actions/interactionsnot-selectedNo current action scope
Retargeted/customizable charactersnot-selectedNo target character supplied
Motion matching/searchnot-selectedNo motion database scope
Networked movementnot-selectedNo controller scope
Runtime performancenot-selectedNo runtime build

Pack inventory and content evidence

The scrubbed inventory records 385 regular files totaling 296,205,291 bytes; 179 are FBX AnimSmith-input candidates. File names indicate locomotion, cover, airborne, turn, and action families, but names are not current semantic proof.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
Baseline mechanical health179/179 FBXsMechanical, rig, timing, and lint assessment completedobserved-animsmith; 716 commands completed
Declared-contract results177/177 motion-labelled files58 pass and 119 fail per output format; not an engine or artistic acceptance resultobserved-animsmith

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
36 negative-time findings in 12 inputsDeclared transform --slice windows at 30 FPS12/12 generated candidates; the 36 time-monotonic errors are absent after slicingEvery output passed inspect, JSON measure, and fix --dry-run; diff exited 1 for the intended changeRemaining loop-seam findings; candidates are unpromoted and no engine/visual result exists
In-place locomotion phase disagreementDeclared transform --gait-anchor on 24 selected in-place members24/24 generated candidates; circular spreads changed 0.7156245→0.0501911, 0.4630161→0.0938395, and 0.6597812→0.0724415Every output passed inspect, JSON measure, and fix --dry-run; diff exited 1 for the intended changePost-output declared lint findings remain; no root-motion, engine, visual, or gameplay acceptance is claimed
Dense constant tracksDeclared transform --prune-constant-tracks on three representatives3/3 generated candidatesEvery output passed inspect, JSON measure, and fix --dry-run; diff exited 1 for the intended changeSemantic/runtime equivalence is unproved; candidates are unpromoted

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityunspecifiedNone in current runnot evaluatedDisposable import, controller, and visual test
Unreal EngineunspecifiedNone in current runnot evaluatedDisposable import, retarget, graph, and visual test
GodotunspecifiedNone in current runnot evaluatedDisposable conversion/import and graph test
BevyunspecifiedNone in current runnot evaluatedSelected export handoff, runtime, and performance test

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Basic Locomotion/current evaluatornot evaluatednot evaluatednot evaluatednot evaluatedCurrent baseline evidence

Limitations and unknowns

  1. No current engine, visual, contact, retarget, or artistic test ran.
  2. Commercial sources and outputs remain external and are not published.

Changes between AnimSmith versions

AnimSmith 0.10.0 — official baseline reran the corpus with the verified release, then produced the current 39-candidate remediation evidence. Earlier 0.7.0 evaluator and any engine/offline results are superseded historical evidence and are not used by the current report.

Reproduction

Evaluator archive: official Linux release archive, SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith, tag v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f, binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387. Working tree: N/A (official archive). The binary had the expected fbx and report feature surface; version, top-level help, and help for inspect, measure, lint, transform, diff, and fix all succeeded, as did representative FBX admission. Current outputs are output schema v19 / measurements schema v18. The safe external evidence locators are external:protofactor-remediation-0.10.0/basic-locomotion/remediation-v0.10.0-final/preflight.json (SHA-256 4fdb2d20e3067c47342cac542d28e7cd5e3272f01843a302827fff5c94af86c9), remediation-results.json (523cca49b496690cf47e7301a72e3839e893721b4acc7e9430e670c830480ea3), and public-summary.json (0574ba377f74d2063cfba8d630f15f595d862ca2bc5ed217984dc8eb2128d466). Each of the 179 source files received inspect, JSON measure, JSON lint, and Markdown lint; all remediation command output and digests remain external.

Sources

Animation pack evaluation: Protofactor Sword and Shield Animset

Technical verdict: Usable with conditions

Evaluation completeness: partial — current mechanical and declared-contract evidence completed; engine, visual, contact, and artistic acceptance were not rerun.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Protofactor Sword and Shield Animset evidence appendix

Technical decision

The current baseline is mechanically readable with AnimSmith 0.10.0, but this pack is not a blanket game-ready asset. 544 baseline commands completed successfully; strict contracts require loop and blend review. The 28 bounded remediation candidates comprise 24 gait-anchor, 3 prune-constant-tracks, and 1 duplicate-loop-endpoint removal transform; all exited 0 with post-output checks recorded in the external evidence projection. They remain external, unpromoted candidates pending engine, contact, and visual review.

Capability coverage

Complete core

  • Source inventory and serial AnimSmith baseline completed without publishing licensed inputs or derivatives.
  • 136 delivered FBX candidates were inspected, measured, and linted in both output formats.
  • Declared contracts and bounded remediation trials were rerun with the current evaluator.

Partial supporting gameplay

  • 132 declared-contract inputs: 34 clean and 230 non-clean command results.
  • Candidate transforms are mechanical evidence, not engine or artistic acceptance.

Absent

  • Current target-engine import/playback, blending, masking, retargeting, contact, performance, and artistic review.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=pack-local; construct project state graphs only from reviewed imported clips.
  2. Timing/synchronization: sync=contract-gated; use loop and seam findings to gate transitions.
  3. State ownership: owner=project-controller; assign root-motion ownership per state.
  4. Composition constraints: composition=full-body-review; validate masks, props, IK, and attachments.
  5. Acceptance gate: gate=engine-visual-contact; require import, playback, contact, and visual sign-off.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
PF-010majorDeclared loop and seam contracts do not pass for the full selected corpus; clip-level admission is required. Guidance.unknownKeep non-clean clips out of production runtime sets.Generic validation and conforming assistance may help.observed-animsmith; current contract evidence.
PF-011minorGenerated candidates are mechanical signals, not visual acceptance. Guidance.unknownReview deltas and playback.Generic track cleanup is applicable.observed-animsmith; 28/28 transforms exit 0.

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unitynot-evaluatedNo current import or playback was run.Disposable-project import and visual controller tests.
Unreal Enginenot-evaluatedNo current import or playback was run.Import, retarget, and blend tests.
Godotnot-evaluatedNo current import or playback was run.Conversion/import route and runtime tests.
Bevynot-evaluatedNo current import or playback was run.Conversion, loading, graph, and performance tests.

Fit and limitations

This is current mechanical evidence, not a guarantee of engine readiness, gameplay suitability, or artistic quality. Licensed source, candidate outputs, and local evidence remain private.

Changes between AnimSmith versions

AnimSmith 0.10.0 — revalidated current inventory, baseline, declared contracts, and bounded remediation using the official FBX-capable release binary. AnimSmith 0.7.0 — retained historical evidence is superseded for current-state conclusions.

Evidence status

See the readiness ladder and appendix. Mechanical completion is separate from engine and visual acceptance.

Sources

Animation pack evidence appendix: Protofactor Sword and Shield Animset

Companion report: technical evaluation

Evidence status: partial — current source inventory, serial mechanical baseline, declared contracts, and bounded remediation completed; engine and artistic acceptance did not.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Current evidence boundary; the canonical readiness ladder remains authoritative.

Evaluation scope and provenance

FieldValue
Pack/editionLocal licensed constituent; revision unknown
Vendor/sourceProtofactor current product listing
Delivered scope136 FBX input candidates
Target useEngine use; no target controller supplied
Target enginesNot evaluated
Target rigs/packsNot evaluated
Source manifestExternal source inventory, SHA-256 e4dc4740bf35ff2812e81ff78970fc6737e62e8022664643c14b5cb8fdf2e4b8
Evaluation manifesturn:animsmith:skill:animation-pack-evaluation-manifest:1
Acquisition/license provenanceAuthorized local commercial input; no legal advice

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files136136Mechanical baseline completedN/A
Rigs/export variants132 animation-bearing candidatespartialMechanical inspection onlyRuntime compatibility not run
AnimSmith baseline136136544 baseline commands completed successfully; strict contracts require loop and blend review.N/A
Declared contracts132 selected motion inputs132132 declared-contract inputs: 34 clean and 230 non-clean command results.Engine behavior is out of scope
Remediation candidates282824 gait-anchor + 3 prune-constant-tracks + 1 duplicate-loop-endpoint removal; all transforms exit 0External candidates remain unpromoted; engine/visual review pending
Engine import/playbackUnknown00Not run
Blend/mask/retargetUnknown00Not run

Claim legend

Uses observed-file, observed-animsmith, and not-evaluated.

Evaluation manifest and taxonomy

The current manifest follows urn:animsmith:skill:animation-pack-evaluation-manifest:1. File inventory is current; gameplay classification is not promoted without engine acceptance.

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00runtime classification withheld
continuous-locomotion00runtime classification withheld
locomotion-transition00runtime classification withheld
airborne00runtime classification withheld
traversal00runtime classification withheld
action-interaction00runtime classification withheld
reaction-death00runtime classification withheld
emote-cinematic00runtime classification withheld
other-unknown00runtime classification withheld
Total00Current runtime classification withheld

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquireevaluated-cleanExternal source retained
Preserve rawevaluated-cleanNew external evidence root
Inspectevaluated-cleanCurrent baseline completed
Segmentnot-evaluatedRuntime grouping not accepted
Root motionnot-evaluatedNo engine controller test
Conformpartially-evaluatedBounded candidate transforms only
Validatepartially-evaluatedBaseline and declared contracts complete
Optimizenot-evaluatedNo production optimization review
Exportnot-evaluatedNo target-engine export accepted
Gate/reportpartially-evaluatedEngine and artistic gates remain

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
All delivered candidatesMechanical baseline completeNot evaluatedNo current engine or artistic acceptance

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — vendor-intendedInventory and baseline completed.
Blended locomotionselected — evaluator-selected-generic-scenarioStrict contract findings require targeted review.
Root-motion controllernot-selectedNo controller was supplied.
State-machine transitionsselected — evaluator-selected-generic-scenarioContract evidence captured; engine validation pending.
Layered upper body/weaponsnot-selectedNo mask or attachment test.
Traversal/environmentnot-selectedNo target scene.
Contact actions/interactionsnot-selectedNo target contacts.
Retargeted/customizable charactersnot-selectedNo target rig.
Motion matching/searchnot-selectedNo target database.
Networked movementnot-selectedNo target networking design.
Runtime performancenot-selectedNo runtime benchmark.

Pack inventory and content evidence

Current inventory found 136 FBX candidates, including 132 animation-bearing candidates, 132 individual-motion candidates, and 0 combined-take candidates. This establishes source presence and mechanical coverage only.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
Baseline commands136/136Inputs are mechanically readable by the current evaluatorobserved-animsmith; 544 baseline commands completed successfully; strict contracts require loop and blend review.
Declared contracts132 selected inputsNon-clean strict contract results require clip-level admissionobserved-animsmith; 132 declared-contract inputs: 34 clean and 230 non-clean command results.

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Mechanical cleanup and gait candidates24 gait-anchor, 3 prune-constant-tracks, 1 duplicate-loop-endpoint removal28/28 transform commands exit 0Post-output inspect/measure/diff/config-lint recorded in external:protofactor-melee-remediation-0.10.0/public-evidence-projection.json (SHA-256 4004862e2f2f7f3719e796c2cedb9d3c264eb943588d5cb2ae78baf917737ac7)External candidates remain unpromoted; no engine, visual, or artistic acceptance

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityNot evaluatedNo current importNot evaluatedImport and visual review
Unreal EngineNot evaluatedNo current importNot evaluatedImport and retarget review
GodotNot evaluatedNo current importNot evaluatedConversion and runtime review
BevyNot evaluatedNo current importNot evaluatedConversion and runtime review

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Delivered packMechanical inspection onlyNot evaluatedNot evaluatedStrict contracts pending reviewNo engine compatibility claim

Limitations and unknowns

  1. Tool evidence does not replace target-engine import, playback, contact, retargeting, masking, or visual review.
  2. Strict declared-contract failures are clip-level admission signals, not a blanket statement about authored quality.
  3. Candidate transforms remain external and require project acceptance before use.

Changes between AnimSmith versions

AnimSmith 0.10.0 — revalidated inventory, baseline, contracts, and bounded remediations with the official FBX-capable evaluator. AnimSmith 0.7.0 — historical evidence is retained only for history and is superseded for current-state conclusions.

Reproduction

Official release archive URL https://github.com/mmannerm/animsmith/releases/download/v0.10.0/animsmith-v0.10.0-x86_64-unknown-linux-gnu.tar.gz; archive SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith; evaluator binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387; tag v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f; working tree N/A; compiled features fbx/report. Expected command/help surface and representative FBX admission succeeded before the exhaustive serial batch. Current operation trace and scrubbed preflight are recorded at external:protofactor-melee-remediation-0.10.0/public-evidence-projection.json (SHA-256 4004862e2f2f7f3719e796c2cedb9d3c264eb943588d5cb2ae78baf917737ac7). No licensed asset, derivative, private path, or output file is published.

Sources

Animation pack evaluation: Protofactor Campfire

Technical verdict: Usable with conditions

Evaluation completeness: partial — all current file and contract checks ran; no current engine or visual acceptance ran.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Protofactor Campfire evidence

Technical decision

Official AnimSmith 0.10.0 loads all 29 delivered FBXs. The untouched baseline has 3,664 constant-track notes across 27 animated files and no errors. Contracts cover 25 motion files: 17 pass and 8 fail; findings are one loop-closure, eight rotational loop-seam, and six velocity loop-seam errors. One current constant-track candidate was generated and mechanically verified as part of the combined 17-candidate remediation rerun; it remains unpromoted. These mechanical conditions do not establish prop contact, loops, or gameplay acceptance.

Capability coverage

Complete core

  • The delivered files contain idle, posture-transition, fire-lighting, food, and log-toss filename families.

Partial supporting gameplay

  • Current contract evidence identifies loop continuity gates; the one external pruning candidate has no contact or attachment acceptance.

Absent

  • No current locomotion, masking, IK, retarget, or engine acceptance result exists.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=not-evaluated; declare posture chains after clip review.
  2. Timing/synchronization: sync=not-evaluated; resolve failed loop contracts first.
  3. State ownership: owner=not-evaluated; motion ownership is not measured for this interaction pack.
  4. Composition constraints: composition=full-body; do not approve masks or prop contact.
  5. Acceptance gate: gate=engine-and-visual-review; test transitions, contacts, props, and build behavior.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
CF-010majorLoop continuity findings can produce wrap pulses.artist-authorReview intended loops and correct/re-export motion where needed.Current declared checks identify the gate; no artistic repair is implied.observed-animsmith; 8 contract failures.

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unity unspecifiednot-evaluatedNo current import or playback.Import, contacts, visual, and build tests.
Unreal Engine unspecifiednot-evaluatedNo current import or playback.Import, retarget, contacts, and build tests.
Godot unspecifiednot-evaluatedNo current conversion or playback.Conversion/import and graph tests.
Bevy unspecifiednot-evaluatedNo current handoff or runtime test.glTF handoff and runtime test.

Fit and limitations

Use only after loop, prop, contact, and visual gates are accepted. Cross-pack compatibility remains untested.

Changes between AnimSmith versions

AnimSmith 0.10.0 — official release revalidated all 29 FBXs and 25 declared contracts, then ran one constant-track pruning candidate in the combined remediation pass. Earlier 0.7.0 evaluator and any engine or offline evidence are historical only.

Evidence status

Current evidence uses the official release and the canonical readiness ladder; commercial artifacts remain external.

Sources

Animation pack evidence appendix: Protofactor Campfire

Companion report: Protofactor Campfire report

Evidence status: partial — the official evaluator loaded the delivered FBXs and produced current baseline evidence.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

The canonical readiness ladder remains authoritative.

Evaluation manifest schema: urn:animsmith:skill:animation-pack-evaluation-manifest:1.

Evaluation scope and provenance

FieldValue
Pack/editionProtofactor Campfire; local constituent revision unknown
Vendor/sourceProtofactor Animset: Campfire
Delivered scopeAuthorized local commercial delivery; 29 FBXs
Target useEngine-neutral campfire interaction intake
Target enginesUnity, Unreal Engine, Godot, and Bevy; not evaluated
Target rigs/packsNo target character or current cross-pack run supplied
Source manifestExternal scrubbed inventory, SHA-256 122e9fe58cb2c5985d861724a41d709e28cf0f2f48408181e848b69b4d954338
Evaluation manifestUnavailable: current output binding was not rendered for this retained report format
Acquisition/license provenanceAuthorized commercial bytes; no transaction or redistribution conclusion

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files29 FBXs29baseline completedmechanical and contract evidence captured
Rigs/export variantsUnknown00Not evaluated in this run
AnimSmith baseline2929current findings recordedinspect, JSON measure, JSON lint, and Markdown lint completed per file
Declared contracts25 motion-labelled files2517 pass; 8 fail per output formatMechanical contract results are current; semantic taxonomy, engine, and artistic acceptance were not evaluated
Offline visual reports000Not evaluated in this run
Engine import/playback4 runtimes00Deferred
Blend/mask/retargetUnknown00Measurements completed, but no target rig or blend/mask acceptance test ran

Claim legend

Current claims are observed-file, observed-animsmith, documentation-stated, or not-evaluated.

Evaluation manifest and taxonomy

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00Classification was not regenerated or accepted
continuous-locomotion00Classification was not regenerated or accepted
locomotion-transition00Classification was not regenerated or accepted
airborne00Classification was not regenerated or accepted
traversal00Classification was not regenerated or accepted
action-interaction00Classification was not regenerated or accepted
reaction-death00Classification was not regenerated or accepted
emote-cinematic00Classification was not regenerated or accepted
other-unknown00Classification was not regenerated or accepted
Total0029 delivered FBXs remain unclassified

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquirepartially-evaluatedAuthorized local inventory
Preserve rawevaluated-cleanSource unchanged
Inspectevaluated-finding29 baseline attempts completed
Segmentnot-evaluatedNo separate segmentation trial selected
Root motionnot-evaluatedMeasurements completed; no root-motion policy or controller acceptance was evaluated
Conformnot-evaluatedNo target rig or conformance policy was selected
Validateevaluated-findingLint completed for every FBX
Optimizepartially-evaluatedOne external constant-track candidate was generated; source was unchanged and runtime equivalence remains unproved
Exportpartially-evaluatedCandidate was written as an external GLB only; no engine handoff was selected
Gate/reportpartially-evaluatedCurrent baseline, remediation, and boundary recorded

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
Entire delivered corpusevaluated-findingnot-evaluatednot-evaluated

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — evaluator-selected-generic-scenarioInventory and current baseline captured
Blended locomotionnot-selectedNo current locomotion scope
Root-motion controllernot-selectedNo root-motion controller scope
State-machine transitionsselected — evaluator-selected-generic-scenarioRequires contract-qualified clip selection and engine test
Layered upper body/weaponsnot-selectedNo current action scope
Traversal/environmentnot-selectedNo traversal scope
Contact actions/interactionsselected — evaluator-selected-generic-scenarioRequires declared contact/prop criteria and engine/visual review
Retargeted/customizable charactersnot-selectedNo target rig supplied
Motion matching/searchnot-selectedNo database scope
Networked movementnot-selectedNo controller scope
Runtime performancenot-selectedNo runtime build

Pack inventory and content evidence

The scrubbed inventory records 114 regular files totaling 188,335,953 bytes; 29 are FBX candidates. Filename labels are not semantic or contact evidence.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
Baseline mechanical health29/29 FBXsMechanical, rig, timing, and lint assessment completedobserved-animsmith; 116 commands completed
Declared-contract results25/25 motion-labelled files17 pass and 8 fail per output format; not an engine or artistic acceptance resultobserved-animsmith

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Dense constant tracksDeclared transform --prune-constant-tracks on one representative1/1 external candidate generated in the combined 17-candidate rerunTransform, inspect, and JSON measure exited 0; diff exited 1 for the intended changeRetained lint findings; runtime equivalence, engine behavior, and visual acceptance are unproved

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityunspecifiedNone in current runnot evaluatedDisposable import, visual interaction, contact, and build test
Unreal EngineunspecifiedNone in current runnot evaluatedDisposable import, retarget, contact, and build test
GodotunspecifiedNone in current runnot evaluatedDisposable conversion/import and graph test
BevyunspecifiedNone in current runnot evaluatedSelected export handoff and runtime test

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Campfire/current evaluatornot evaluatednot evaluatednot evaluatednot evaluatedCurrent baseline evidence

Limitations and unknowns

  1. No current engine, visual, contact, retarget, or artistic test ran.
  2. Commercial sources and derivatives remain external.

Changes between AnimSmith versions

AnimSmith 0.10.0 — exhaustive current baseline reran all 29 FBXs with the verified official release, then produced one current pruning candidate in the combined remediation pass. Earlier 0.7.0 evaluator and any engine/offline results are historical only.

Reproduction

Evaluator archive: official Linux release archive, SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith, tag v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f, binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387. Working tree: N/A (official archive). The binary had the expected fbx and report feature surface; version, top-level help, and help for inspect, measure, lint, transform, diff, and fix all succeeded, as did representative FBX admission. Current outputs are output schema v19 / measurements schema v18. Safe evidence is external at external:protofactor-remediation-0.10.0/: preflight SHA-256 dde0aa485d392c360736346041558b72f39798de7fdcc443ad7aaba9fc8b445a, status SHA-256 d85a1c53d5d1fcb829e0574051f173f6e1c08a409358385f4400940e11652e1f, and ledger SHA-256 fdab771891c1ee7d316bab224f0dc2bb65cd286f84b21c9c91cab171f580f5ec. All transform, inspect, and JSON measure invocations exited 0; diff exited 1 for intentional output changes. Retained lint findings and all candidate payloads remain external.

Sources

Animation pack evaluation: Protofactor Climbing Animset

Technical verdict: Usable with conditions

Evaluation completeness: partial — all current file and contract checks ran; no current engine or visual acceptance ran.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Protofactor Climbing evidence

Technical decision

Official AnimSmith 0.10.0 loads all 77 delivered FBXs. The untouched baseline has 9,011 constant-track notes and no errors. Contracts cover 75 motion files: 34 pass and 41 fail, with 26 loop-closure, 41 rotational loop-seam, and 39 velocity loop-seam errors. One current constant-track candidate was generated and mechanically verified as part of the combined 17-candidate remediation rerun; it remains unpromoted. No current traversal, contact, root-motion, engine, or visual test establishes runtime acceptance.

Capability coverage

Complete core

  • The delivery has wall, ladder, obstacle, fall, jump, landing, entry, and exit filename families.

Partial supporting gameplay

  • Mechanical and declared-loop evidence is available, but traversal contacts and controller behavior are untested.

Absent

  • No current IK, retarget, engine, or artistic acceptance is established.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=not-evaluated; declare traversal chains after clip review.
  2. Timing/synchronization: sync=not-evaluated; resolve failed loop contracts first.
  3. State ownership: owner=not-evaluated; declare root-motion/controller ownership.
  4. Composition constraints: composition=full-body; do not approve wall or ladder contacts.
  5. Acceptance gate: gate=engine-and-visual-review; test traversal, contacts, IK, and build behavior.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
CL-010majorLoop continuity findings can cause transition pulses.artist-authorReview intended loops and correct/re-export motion.Current checks identify the gate; no artistic repair is implied.observed-animsmith; 41 contract failures.

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unity unspecifiednot-evaluatedNo current import or traversal playback.Controller, contact, visual, and build tests.
Unreal Engine unspecifiednot-evaluatedNo current import or traversal playback.Retarget, IK, contact, and build tests.
Godot unspecifiednot-evaluatedNo current conversion or playback.Conversion/import and graph tests.
Bevy unspecifiednot-evaluatedNo current handoff or runtime test.glTF handoff and runtime test.

Fit and limitations

Use only after target-engine traversal and contact acceptance. Cross-pack and artistic compatibility remain untested.

Changes between AnimSmith versions

AnimSmith 0.10.0 — official release revalidated all 77 FBXs and 75 declared contracts, then ran one constant-track pruning candidate in the combined remediation pass. Earlier 0.7.0 evaluator and any engine or offline evidence are historical only.

Evidence status

Current evidence uses the official release and the canonical readiness ladder; commercial artifacts remain external.

Sources

Animation pack evidence appendix: Protofactor Climbing Animset

Companion report: Protofactor Climbing report

Evidence status: partial — the official evaluator loaded the delivered FBXs and produced current baseline evidence.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

The canonical readiness ladder remains authoritative.

Evaluation manifest schema: urn:animsmith:skill:animation-pack-evaluation-manifest:1.

Evaluation scope and provenance

FieldValue
Pack/editionProtofactor Climbing Animset; local constituent revision unknown
Vendor/sourceProtofactor Animset: Climbing
Delivered scopeAuthorized local commercial delivery; 77 FBXs
Target useEngine-neutral traversal intake
Target enginesUnity, Unreal Engine, Godot, and Bevy; not evaluated
Target rigs/packsNo target character or current cross-pack run supplied
Source manifestExternal scrubbed inventory, SHA-256 9646d3aa9057fbfe58d612642d1a8c6b72039a5c0f6fec8ee60f27d0fb66c12a
Evaluation manifestUnavailable: current output binding was not rendered for this retained report format
Acquisition/license provenanceAuthorized commercial bytes; no transaction or redistribution conclusion

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files77 FBXs77baseline completedmechanical and contract evidence captured
Rigs/export variantsUnknown00Not evaluated in this run
AnimSmith baseline7777current findings recordedinspect, JSON measure, JSON lint, and Markdown lint completed per file
Declared contracts75 motion-labelled files7534 pass; 41 fail per output formatMechanical contract results are current; semantic taxonomy, engine, and artistic acceptance were not evaluated
Offline visual reports000Not evaluated in this run
Engine import/playback4 runtimes00Deferred
Blend/mask/retargetUnknown00Measurements completed, but no target rig or blend/mask acceptance test ran

Claim legend

Current claims are observed-file, observed-animsmith, documentation-stated, or not-evaluated.

Evaluation manifest and taxonomy

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00Classification was not regenerated or accepted
continuous-locomotion00Classification was not regenerated or accepted
locomotion-transition00Classification was not regenerated or accepted
airborne00Classification was not regenerated or accepted
traversal00Classification was not regenerated or accepted
action-interaction00Classification was not regenerated or accepted
reaction-death00Classification was not regenerated or accepted
emote-cinematic00Classification was not regenerated or accepted
other-unknown00Classification was not regenerated or accepted
Total0077 delivered FBXs remain unclassified

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquirepartially-evaluatedAuthorized local inventory
Preserve rawevaluated-cleanSource unchanged
Inspectevaluated-finding77 baseline attempts completed
Segmentnot-evaluatedNo separate segmentation trial selected
Root motionnot-evaluatedMeasurements completed; no root-motion policy or controller acceptance was evaluated
Conformnot-evaluatedNo target rig or conformance policy was selected
Validateevaluated-findingLint completed for every FBX
Optimizepartially-evaluatedOne external constant-track candidate was generated; source was unchanged and runtime equivalence remains unproved
Exportpartially-evaluatedCandidate was written as an external GLB only; no engine handoff was selected
Gate/reportpartially-evaluatedCurrent baseline, remediation, and boundary recorded

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
Entire delivered corpusevaluated-findingnot-evaluatednot-evaluated

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — evaluator-selected-generic-scenarioInventory and current baseline captured
Blended locomotionnot-selectedNo current locomotion scope
Root-motion controllerselected — evaluator-selected-generic-scenarioMeasurements completed; controller trajectory policy and engine acceptance remain
State-machine transitionsselected — evaluator-selected-generic-scenarioRequires contract-qualified clip selection and engine test
Layered upper body/weaponsnot-selectedNo current action scope
Traversal/environmentselected — evaluator-selected-generic-scenarioRequires traversal and contact review
Contact actions/interactionsselected — evaluator-selected-generic-scenarioRequires obstacle/ladder evidence
Retargeted/customizable charactersnot-selectedNo target rig supplied
Motion matching/searchnot-selectedNo database scope
Networked movementnot-selectedNo controller scope
Runtime performancenot-selectedNo runtime build

Pack inventory and content evidence

The scrubbed inventory records 179 regular files totaling 194,505,327 bytes; 77 are FBX candidates. Filename labels are not semantic, contact, or root-motion evidence.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
Baseline mechanical health77/77 FBXsMechanical, rig, timing, and lint assessment completedobserved-animsmith; 308 commands completed
Declared-contract results75/75 motion-labelled files34 pass and 41 fail per output format; not an engine or artistic acceptance resultobserved-animsmith

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Dense constant tracksDeclared transform --prune-constant-tracks on one representative1/1 external candidate generated in the combined 17-candidate rerunTransform, inspect, and JSON measure exited 0; diff exited 1 for the intended changeRetained lint findings; runtime equivalence, engine behavior, and visual acceptance are unproved

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityunspecifiedNone in current runnot evaluatedDisposable import, traversal, contact, and visual test
Unreal EngineunspecifiedNone in current runnot evaluatedDisposable import, retarget, IK, contact, and build test
GodotunspecifiedNone in current runnot evaluatedDisposable conversion/import and graph test
BevyunspecifiedNone in current runnot evaluatedSelected export handoff and runtime test

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Climbing/current evaluatornot evaluatednot evaluatednot evaluatednot evaluatedCurrent baseline evidence

Limitations and unknowns

  1. No current engine, visual, contact, retarget, or artistic test ran.
  2. Commercial sources and derivatives remain external.

Changes between AnimSmith versions

AnimSmith 0.10.0 — exhaustive current baseline reran all 77 FBXs with the verified official release, then produced one current pruning candidate in the combined remediation pass. Earlier 0.7.0 evaluator and any engine/offline results are historical only.

Reproduction

Evaluator archive: official Linux release archive, SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith, tag v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f, binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387. Working tree: N/A (official archive). The binary had the expected fbx and report feature surface; version, top-level help, and help for inspect, measure, lint, transform, diff, and fix all succeeded, as did representative FBX admission. Current outputs are output schema v19 / measurements schema v18. Safe evidence is external at external:protofactor-remediation-0.10.0/: preflight SHA-256 dde0aa485d392c360736346041558b72f39798de7fdcc443ad7aaba9fc8b445a, status SHA-256 d85a1c53d5d1fcb829e0574051f173f6e1c08a409358385f4400940e11652e1f, and ledger SHA-256 fdab771891c1ee7d316bab224f0dc2bb65cd286f84b21c9c91cab171f580f5ec. All transform, inspect, and JSON measure invocations exited 0; diff exited 1 for intentional output changes. Retained lint findings and all candidate payloads remain external.

Sources

Animation pack evaluation: Protofactor Injured Animset

Technical verdict: Usable with conditions

Evaluation completeness: partial — all current file and contract checks ran; no current engine or visual acceptance ran.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Protofactor Injured evidence

Technical decision

Official AnimSmith 0.10.0 loads all 72 delivered FBXs. The untouched baseline has 9,915 constant-track notes and no errors. Contracts cover 70 motion files: 28 pass and 42 fail, with 15 loop-closure, 42 rotational loop-seam, and 31 velocity loop-seam errors. Fourteen declared in-place gait candidates and one constant-track candidate were generated and mechanically verified in the combined 17-candidate remediation rerun; all remain unpromoted. No current blend, root-motion, engine, retarget, contact, or visual test establishes acceptance.

Capability coverage

Complete core

  • The delivery has injured idle, kneel, sit, transition, walk, and run filename families.

Partial supporting gameplay

  • Current contract evidence covers 14 declared in-place files. Their external gait candidates are mechanically verified, but loop and blend conditions remain unresolved.

Absent

  • No current recovery, retarget, masking, engine, or artistic acceptance is established.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=not-evaluated; declare injured locomotion rings after clip review.
  2. Timing/synchronization: sync=not-evaluated; resolve failed loop contracts first.
  3. State ownership: owner=not-evaluated; declare IP/RM controller ownership.
  4. Composition constraints: composition=full-body; do not approve injury masks.
  5. Acceptance gate: gate=engine-and-visual-review; test blends, retarget, and gameplay behavior.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
IN-010majorLoop continuity findings can cause visible wrap or transition pulses.artist-authorReview intended loops and correct/re-export motion.Current checks identify the gate; no artistic repair is implied.observed-animsmith; 42 contract failures.

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unity unspecifiednot-evaluatedNo current import or playback.Blend, mask, visual, and build tests.
Unreal Engine unspecifiednot-evaluatedNo current import or playback.Retarget, graph, and build tests.
Godot unspecifiednot-evaluatedNo current conversion or playback.Conversion/import and graph tests.
Bevy unspecifiednot-evaluatedNo current handoff or runtime test.glTF handoff and runtime test.

Fit and limitations

Use only after loop, blend, retarget, and visual acceptance. Cross-pack compatibility remains untested.

Changes between AnimSmith versions

AnimSmith 0.10.0 — official release revalidated all 72 FBXs and 70 declared contracts, then ran 14 declared in-place gait anchors and one constant-track pruning candidate in the combined remediation pass. Earlier 0.7.0 evaluator and any engine or offline evidence are historical only.

Evidence status

Current evidence uses the official release and the canonical readiness ladder; commercial artifacts remain external.

Sources

Animation pack evidence appendix: Protofactor Injured Animset

Companion report: Protofactor Injured report

Evidence status: partial — the official evaluator loaded the delivered FBXs and produced current baseline evidence.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

The canonical readiness ladder remains authoritative.

Evaluation manifest schema: urn:animsmith:skill:animation-pack-evaluation-manifest:1.

Evaluation scope and provenance

FieldValue
Pack/editionProtofactor Injured Animset; local constituent revision unknown
Vendor/sourceProtofactor Animset: Injured
Delivered scopeAuthorized local commercial delivery; 72 FBXs
Target useEngine-neutral injured-state intake
Target enginesUnity, Unreal Engine, Godot, and Bevy; not evaluated
Target rigs/packsNo target character or current cross-pack run supplied
Source manifestExternal scrubbed inventory, SHA-256 e60ef65d2964a9eaa497cb6f8ee898542f88a67719625633cfee64ad7816e7cf
Evaluation manifestUnavailable: current output binding was not rendered for this retained report format
Acquisition/license provenanceAuthorized commercial bytes; no transaction or redistribution conclusion

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files72 FBXs72baseline completedmechanical and contract evidence captured
Rigs/export variantsUnknown00Not evaluated in this run
AnimSmith baseline7272current findings recordedinspect, JSON measure, JSON lint, and Markdown lint completed per file
Declared contracts70 motion-labelled files7028 pass; 42 fail per output formatMechanical contract results are current; semantic taxonomy, engine, and artistic acceptance were not evaluated
Offline visual reports000Not evaluated in this run
Engine import/playback4 runtimes00Deferred
Blend/mask/retargetUnknown00Measurements completed, but no target rig or blend/mask acceptance test ran

Claim legend

Current claims are observed-file, observed-animsmith, documentation-stated, or not-evaluated.

Evaluation manifest and taxonomy

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00Classification was not regenerated or accepted
continuous-locomotion00Classification was not regenerated or accepted
locomotion-transition00Classification was not regenerated or accepted
airborne00Classification was not regenerated or accepted
traversal00Classification was not regenerated or accepted
action-interaction00Classification was not regenerated or accepted
reaction-death00Classification was not regenerated or accepted
emote-cinematic00Classification was not regenerated or accepted
other-unknown00Classification was not regenerated or accepted
Total0072 delivered FBXs remain unclassified

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquirepartially-evaluatedAuthorized local inventory
Preserve rawevaluated-cleanSource unchanged
Inspectevaluated-finding72 baseline attempts completed
Segmentnot-evaluatedNo separate segmentation trial selected
Root motionnot-evaluatedMeasurements completed; no root-motion policy or controller acceptance was evaluated
Conformpartially-evaluatedFourteen declared in-place gait anchors generated external candidates; no target-rig or engine acceptance was run
Validateevaluated-findingLint completed for every FBX
Optimizepartially-evaluatedOne external constant-track candidate was generated; source was unchanged and runtime equivalence remains unproved
Exportpartially-evaluatedCandidates were written as external GLBs only; no engine handoff was selected
Gate/reportpartially-evaluatedCurrent baseline, remediation, and boundary recorded

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
Entire delivered corpusevaluated-findingnot-evaluatednot-evaluated

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — evaluator-selected-generic-scenarioInventory and current baseline captured
Blended locomotionselected — evaluator-selected-generic-scenarioMeasurements completed; selected IP/RM ownership, blend policy, and engine acceptance remain
Root-motion controllerselected — evaluator-selected-generic-scenarioMeasurements completed; controller trajectory policy and engine acceptance remain
State-machine transitionsselected — evaluator-selected-generic-scenarioRequires contract-qualified clip selection and engine test
Layered upper body/weaponsnot-selectedNo current action scope
Traversal/environmentnot-selectedNo traversal scope
Contact actions/interactionsnot-selectedNo current contact scope
Retargeted/customizable charactersnot-selectedNo target rig supplied
Motion matching/searchnot-selectedNo database scope
Networked movementnot-selectedNo controller scope
Runtime performancenot-selectedNo runtime build

Pack inventory and content evidence

The scrubbed inventory records 171 regular files totaling 217,461,232 bytes; 72 are FBX candidates. Filename labels are not semantic, speed, loop, or blend evidence.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
Baseline mechanical health72/72 FBXsMechanical, rig, timing, and lint assessment completedobserved-animsmith; 288 commands completed
Declared-contract results70/70 motion-labelled files28 pass and 42 fail per output format; not an engine or artistic acceptance resultobserved-animsmith

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
In-place locomotion phase alignmentDeclared transform --gait-anchor on 14 in-place members14/14 external candidates generated in the combined 17-candidate rerunTransform, inspect, and JSON measure exited 0; diff exited 1 for intended changesRetained lint findings; no engine, visual, root-motion, or gameplay acceptance is claimed
Dense constant tracksDeclared transform --prune-constant-tracks on one representative1/1 external candidate generatedTransform, inspect, and JSON measure exited 0; diff exited 1 for the intended changeRuntime equivalence is unproved; candidate is unpromoted

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityunspecifiedNone in current runnot evaluatedDisposable import, blend, mask, and visual test
Unreal EngineunspecifiedNone in current runnot evaluatedDisposable import, retarget, graph, and build test
GodotunspecifiedNone in current runnot evaluatedDisposable conversion/import and graph test
BevyunspecifiedNone in current runnot evaluatedSelected export handoff and runtime test

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Injured/current evaluatornot evaluatednot evaluatednot evaluatednot evaluatedCurrent baseline evidence

Limitations and unknowns

  1. No current engine, visual, contact, retarget, or artistic test ran.
  2. Commercial sources and derivatives remain external.

Changes between AnimSmith versions

AnimSmith 0.10.0 — exhaustive current baseline reran all 72 FBXs with the verified official release, then produced 14 gait-anchor and one pruning candidate in the combined remediation pass. Earlier 0.7.0 evaluator and any engine/offline results are historical only.

Reproduction

Evaluator archive: official Linux release archive, SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith, tag v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f, binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387. Working tree: N/A (official archive). The binary had the expected fbx and report feature surface; version, top-level help, and help for inspect, measure, lint, transform, diff, and fix all succeeded, as did representative FBX admission. Current outputs are output schema v19 / measurements schema v18. Safe evidence is external at external:protofactor-remediation-0.10.0/: preflight SHA-256 dde0aa485d392c360736346041558b72f39798de7fdcc443ad7aaba9fc8b445a, status SHA-256 d85a1c53d5d1fcb829e0574051f173f6e1c08a409358385f4400940e11652e1f, and ledger SHA-256 fdab771891c1ee7d316bab224f0dc2bb65cd286f84b21c9c91cab171f580f5ec. All transform, inspect, and JSON measure invocations exited 0; diff exited 1 for intentional output changes. Retained lint findings and all candidate payloads remain external.

Sources

Animation pack evaluation: Protofactor One-Handed Melee Animset

Technical verdict: Usable with conditions

Evaluation completeness: partial — current mechanical and declared-contract evidence completed; engine, visual, contact, and artistic acceptance were not rerun.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Protofactor One-Handed Melee Animset evidence appendix

Technical decision

The current baseline is mechanically readable with AnimSmith 0.10.0, but this pack is not a blanket game-ready asset. 452 baseline commands completed successfully; constant-track notes and strict loop contracts require review. The 25 bounded remediation candidates comprise 24 gait-anchor and 1 prune-constant-tracks transform; all exited 0 with post-output checks recorded in the external evidence projection. They remain external, unpromoted candidates pending engine, contact, and visual review.

Capability coverage

Complete core

  • Source inventory and serial AnimSmith baseline completed without publishing licensed inputs or derivatives.
  • 113 delivered FBX candidates were inspected, measured, and linted in both output formats.
  • Declared contracts and bounded remediation trials were rerun with the current evaluator.

Partial supporting gameplay

  • 110 declared-contract inputs: 23 clean and 87 non-clean results in each output format.
  • Candidate transforms are mechanical evidence, not engine or artistic acceptance.

Absent

  • Current target-engine import/playback, blending, masking, retargeting, contact, performance, and artistic review.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=pack-local; construct project state graphs only from reviewed imported clips.
  2. Timing/synchronization: sync=contract-gated; use loop and seam findings to gate transitions.
  3. State ownership: owner=project-controller; assign root-motion ownership per state.
  4. Composition constraints: composition=full-body-review; validate masks, props, IK, and attachments.
  5. Acceptance gate: gate=engine-visual-contact; require import, playback, contact, and visual sign-off.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
PF-010majorDeclared loop and seam contracts do not pass for the full selected corpus; clip-level admission is required. Guidance.unknownKeep non-clean clips out of production runtime sets.Generic validation and conforming assistance may help.observed-animsmith; current contract evidence.
PF-011minorGenerated candidates are mechanical signals, not visual acceptance. Guidance.unknownReview deltas and playback.Generic track cleanup is applicable.observed-animsmith; 25/25 transforms exit 0.

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unitynot-evaluatedNo current import or playback was run.Disposable-project import and visual controller tests.
Unreal Enginenot-evaluatedNo current import or playback was run.Import, retarget, and blend tests.
Godotnot-evaluatedNo current import or playback was run.Conversion/import route and runtime tests.
Bevynot-evaluatedNo current import or playback was run.Conversion, loading, graph, and performance tests.

Fit and limitations

This is current mechanical evidence, not a guarantee of engine readiness, gameplay suitability, or artistic quality. Licensed source, candidate outputs, and local evidence remain private.

Changes between AnimSmith versions

AnimSmith 0.10.0 — revalidated current inventory, baseline, declared contracts, and bounded remediation using the official FBX-capable release binary. AnimSmith 0.7.0 — retained historical evidence is superseded for current-state conclusions.

Evidence status

See the readiness ladder and appendix. Mechanical completion is separate from engine and visual acceptance.

Sources

Animation pack evidence appendix: Protofactor One-Handed Melee Animset

Companion report: technical evaluation

Evidence status: partial — current source inventory, serial mechanical baseline, declared contracts, and bounded remediation completed; engine and artistic acceptance did not.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Current evidence boundary; the canonical readiness ladder remains authoritative.

Evaluation scope and provenance

FieldValue
Pack/editionLocal licensed constituent; revision unknown
Vendor/sourceProtofactor current product listing
Delivered scope113 FBX input candidates
Target useEngine use; no target controller supplied
Target enginesNot evaluated
Target rigs/packsNot evaluated
Source manifestExternal source inventory, SHA-256 d70f848f52c11918d90f02f460455cc98b20cacda292cc5b7ae71f733027a5a7
Evaluation manifesturn:animsmith:skill:animation-pack-evaluation-manifest:1
Acquisition/license provenanceAuthorized local commercial input; no legal advice

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files113113Mechanical baseline completedN/A
Rigs/export variants112 animation-bearing candidatespartialMechanical inspection onlyRuntime compatibility not run
AnimSmith baseline113113452 baseline commands completed successfully; constant-track notes and strict loop contracts require review.N/A
Declared contracts110 selected motion inputs110110 declared-contract inputs: 23 clean and 87 non-clean results in each output format.Engine behavior is out of scope
Remediation candidates252524 gait-anchor + 1 prune-constant-tracks; all transforms exit 0External candidates remain unpromoted; engine/visual review pending
Engine import/playbackUnknown00Not run
Blend/mask/retargetUnknown00Not run

Claim legend

Uses observed-file, observed-animsmith, and not-evaluated.

Evaluation manifest and taxonomy

The current manifest follows urn:animsmith:skill:animation-pack-evaluation-manifest:1. File inventory is current; gameplay classification is not promoted without engine acceptance.

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00runtime classification withheld
continuous-locomotion00runtime classification withheld
locomotion-transition00runtime classification withheld
airborne00runtime classification withheld
traversal00runtime classification withheld
action-interaction00runtime classification withheld
reaction-death00runtime classification withheld
emote-cinematic00runtime classification withheld
other-unknown00runtime classification withheld
Total00Current runtime classification withheld

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquireevaluated-cleanExternal source retained
Preserve rawevaluated-cleanNew external evidence root
Inspectevaluated-cleanCurrent baseline completed
Segmentnot-evaluatedRuntime grouping not accepted
Root motionnot-evaluatedNo engine controller test
Conformpartially-evaluatedBounded candidate transforms only
Validatepartially-evaluatedBaseline and declared contracts complete
Optimizenot-evaluatedNo production optimization review
Exportnot-evaluatedNo target-engine export accepted
Gate/reportpartially-evaluatedEngine and artistic gates remain

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
All delivered candidatesMechanical baseline completeNot evaluatedNo current engine or artistic acceptance

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — vendor-intendedInventory and baseline completed.
Blended locomotionselected — evaluator-selected-generic-scenarioStrict contract findings require targeted review.
Root-motion controllernot-selectedNo controller was supplied.
State-machine transitionsselected — evaluator-selected-generic-scenarioContract evidence captured; engine validation pending.
Layered upper body/weaponsnot-selectedNo mask or attachment test.
Traversal/environmentnot-selectedNo target scene.
Contact actions/interactionsnot-selectedNo target contacts.
Retargeted/customizable charactersnot-selectedNo target rig.
Motion matching/searchnot-selectedNo target database.
Networked movementnot-selectedNo target networking design.
Runtime performancenot-selectedNo runtime benchmark.

Pack inventory and content evidence

Current inventory found 113 FBX candidates, including 112 animation-bearing candidates, 110 individual-motion candidates, and 1 combined-take candidates. This establishes source presence and mechanical coverage only.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
Baseline commands113/113Inputs are mechanically readable by the current evaluatorobserved-animsmith; 452 baseline commands completed successfully; constant-track notes and strict loop contracts require review.
Declared contracts110 selected inputsNon-clean strict contract results require clip-level admissionobserved-animsmith; 110 declared-contract inputs: 23 clean and 87 non-clean results in each output format.

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Mechanical cleanup and gait candidates24 gait-anchor + 1 prune-constant-tracks25/25 transform commands exit 0Post-output inspect/measure/diff/config-lint recorded in external:protofactor-melee-remediation-0.10.0/public-evidence-projection.json (SHA-256 4004862e2f2f7f3719e796c2cedb9d3c264eb943588d5cb2ae78baf917737ac7)External candidates remain unpromoted; no engine, visual, or artistic acceptance

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityNot evaluatedNo current importNot evaluatedImport and visual review
Unreal EngineNot evaluatedNo current importNot evaluatedImport and retarget review
GodotNot evaluatedNo current importNot evaluatedConversion and runtime review
BevyNot evaluatedNo current importNot evaluatedConversion and runtime review

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Delivered packMechanical inspection onlyNot evaluatedNot evaluatedStrict contracts pending reviewNo engine compatibility claim

Limitations and unknowns

  1. Tool evidence does not replace target-engine import, playback, contact, retargeting, masking, or visual review.
  2. Strict declared-contract failures are clip-level admission signals, not a blanket statement about authored quality.
  3. Candidate transforms remain external and require project acceptance before use.

Changes between AnimSmith versions

AnimSmith 0.10.0 — revalidated inventory, baseline, contracts, and bounded remediations with the official FBX-capable evaluator. AnimSmith 0.7.0 — historical evidence is retained only for history and is superseded for current-state conclusions.

Reproduction

Official release archive URL https://github.com/mmannerm/animsmith/releases/download/v0.10.0/animsmith-v0.10.0-x86_64-unknown-linux-gnu.tar.gz; archive SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith; evaluator binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387; tag v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f; working tree N/A; compiled features fbx/report. Expected command/help surface and representative FBX admission succeeded before the exhaustive serial batch. Current operation trace and scrubbed preflight are recorded at external:protofactor-melee-remediation-0.10.0/public-evidence-projection.json (SHA-256 4004862e2f2f7f3719e796c2cedb9d3c264eb943588d5cb2ae78baf917737ac7). No licensed asset, derivative, private path, or output file is published.

Sources

Animation pack evaluation: Protofactor Two-Handed Melee Animset

Technical verdict: Usable with conditions

Evaluation completeness: partial — current mechanical and declared-contract evidence completed; engine, visual, contact, and artistic acceptance were not rerun.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Protofactor Two-Handed Melee Animset evidence appendix

Technical decision

The current baseline is mechanically readable with AnimSmith 0.10.0, but this pack is not a blanket game-ready asset. 492 baseline commands completed successfully; constant-track notes, one duration warning, and strict loop contracts require review. The 25 bounded remediation candidates comprise 24 gait-anchor and 1 prune-constant-tracks transform; all exited 0 with post-output checks recorded in the external evidence projection. They remain external, unpromoted candidates pending engine, contact, and visual review.

Capability coverage

Complete core

  • Source inventory and serial AnimSmith baseline completed without publishing licensed inputs or derivatives.
  • 123 delivered FBX candidates were inspected, measured, and linted in both output formats.
  • Declared contracts and bounded remediation trials were rerun with the current evaluator.

Partial supporting gameplay

  • 120 declared-contract inputs: 13 clean and 107 non-clean results in each output format.
  • Candidate transforms are mechanical evidence, not engine or artistic acceptance.

Absent

  • Current target-engine import/playback, blending, masking, retargeting, contact, performance, and artistic review.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=pack-local; construct project state graphs only from reviewed imported clips.
  2. Timing/synchronization: sync=contract-gated; use loop and seam findings to gate transitions.
  3. State ownership: owner=project-controller; assign root-motion ownership per state.
  4. Composition constraints: composition=full-body-review; validate masks, props, IK, and attachments.
  5. Acceptance gate: gate=engine-visual-contact; require import, playback, contact, and visual sign-off.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
PF-010majorDeclared loop and seam contracts do not pass for the full selected corpus; clip-level admission is required. Guidance.unknownKeep non-clean clips out of production runtime sets.Generic validation and conforming assistance may help.observed-animsmith; current contract evidence.
PF-011minorGenerated candidates are mechanical signals, not visual acceptance. Guidance.unknownReview deltas and playback.Generic track cleanup is applicable.observed-animsmith; 25/25 transforms exit 0.

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unitynot-evaluatedNo current import or playback was run.Disposable-project import and visual controller tests.
Unreal Enginenot-evaluatedNo current import or playback was run.Import, retarget, and blend tests.
Godotnot-evaluatedNo current import or playback was run.Conversion/import route and runtime tests.
Bevynot-evaluatedNo current import or playback was run.Conversion, loading, graph, and performance tests.

Fit and limitations

This is current mechanical evidence, not a guarantee of engine readiness, gameplay suitability, or artistic quality. Licensed source, candidate outputs, and local evidence remain private.

Changes between AnimSmith versions

AnimSmith 0.10.0 — revalidated current inventory, baseline, declared contracts, and bounded remediation using the official FBX-capable release binary. AnimSmith 0.7.0 — retained historical evidence is superseded for current-state conclusions.

Evidence status

See the readiness ladder and appendix. Mechanical completion is separate from engine and visual acceptance.

Sources

Animation pack evidence appendix: Protofactor Two-Handed Melee Animset

Companion report: technical evaluation

Evidence status: partial — current source inventory, serial mechanical baseline, declared contracts, and bounded remediation completed; engine and artistic acceptance did not.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Current evidence boundary; the canonical readiness ladder remains authoritative.

Evaluation scope and provenance

FieldValue
Pack/editionLocal licensed constituent; revision unknown
Vendor/sourceProtofactor current product listing
Delivered scope123 FBX input candidates
Target useEngine use; no target controller supplied
Target enginesNot evaluated
Target rigs/packsNot evaluated
Source manifestExternal source inventory, SHA-256 1fc1189eb8ccb299fe26ff51bce96d932b7b2dab5c71a0c6adab477937ad7bc7
Evaluation manifesturn:animsmith:skill:animation-pack-evaluation-manifest:1
Acquisition/license provenanceAuthorized local commercial input; no legal advice

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files123123Mechanical baseline completedN/A
Rigs/export variants122 animation-bearing candidatespartialMechanical inspection onlyRuntime compatibility not run
AnimSmith baseline123123492 baseline commands completed successfully; constant-track notes, one duration warning, and strict loop contracts require review.N/A
Declared contracts120 selected motion inputs120120 declared-contract inputs: 13 clean and 107 non-clean results in each output format.Engine behavior is out of scope
Remediation candidates252524 gait-anchor + 1 prune-constant-tracks; all transforms exit 0External candidates remain unpromoted; engine/visual review pending
Engine import/playbackUnknown00Not run
Blend/mask/retargetUnknown00Not run

Claim legend

Uses observed-file, observed-animsmith, and not-evaluated.

Evaluation manifest and taxonomy

The current manifest follows urn:animsmith:skill:animation-pack-evaluation-manifest:1. File inventory is current; gameplay classification is not promoted without engine acceptance.

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00runtime classification withheld
continuous-locomotion00runtime classification withheld
locomotion-transition00runtime classification withheld
airborne00runtime classification withheld
traversal00runtime classification withheld
action-interaction00runtime classification withheld
reaction-death00runtime classification withheld
emote-cinematic00runtime classification withheld
other-unknown00runtime classification withheld
Total00Current runtime classification withheld

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquireevaluated-cleanExternal source retained
Preserve rawevaluated-cleanNew external evidence root
Inspectevaluated-cleanCurrent baseline completed
Segmentnot-evaluatedRuntime grouping not accepted
Root motionnot-evaluatedNo engine controller test
Conformpartially-evaluatedBounded candidate transforms only
Validatepartially-evaluatedBaseline and declared contracts complete
Optimizenot-evaluatedNo production optimization review
Exportnot-evaluatedNo target-engine export accepted
Gate/reportpartially-evaluatedEngine and artistic gates remain

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
All delivered candidatesMechanical baseline completeNot evaluatedNo current engine or artistic acceptance

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — vendor-intendedInventory and baseline completed.
Blended locomotionselected — evaluator-selected-generic-scenarioStrict contract findings require targeted review.
Root-motion controllernot-selectedNo controller was supplied.
State-machine transitionsselected — evaluator-selected-generic-scenarioContract evidence captured; engine validation pending.
Layered upper body/weaponsnot-selectedNo mask or attachment test.
Traversal/environmentnot-selectedNo target scene.
Contact actions/interactionsnot-selectedNo target contacts.
Retargeted/customizable charactersnot-selectedNo target rig.
Motion matching/searchnot-selectedNo target database.
Networked movementnot-selectedNo target networking design.
Runtime performancenot-selectedNo runtime benchmark.

Pack inventory and content evidence

Current inventory found 123 FBX candidates, including 122 animation-bearing candidates, 120 individual-motion candidates, and 1 combined-take candidates. This establishes source presence and mechanical coverage only.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
Baseline commands123/123Inputs are mechanically readable by the current evaluatorobserved-animsmith; 492 baseline commands completed successfully; constant-track notes, one duration warning, and strict loop contracts require review.
Declared contracts120 selected inputsNon-clean strict contract results require clip-level admissionobserved-animsmith; 120 declared-contract inputs: 13 clean and 107 non-clean results in each output format.

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Mechanical cleanup and gait candidates24 gait-anchor + 1 prune-constant-tracks25/25 transform commands exit 0Post-output inspect/measure/diff/config-lint recorded in external:protofactor-melee-remediation-0.10.0/public-evidence-projection.json (SHA-256 4004862e2f2f7f3719e796c2cedb9d3c264eb943588d5cb2ae78baf917737ac7)External candidates remain unpromoted; no engine, visual, or artistic acceptance

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityNot evaluatedNo current importNot evaluatedImport and visual review
Unreal EngineNot evaluatedNo current importNot evaluatedImport and retarget review
GodotNot evaluatedNo current importNot evaluatedConversion and runtime review
BevyNot evaluatedNo current importNot evaluatedConversion and runtime review

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Delivered packMechanical inspection onlyNot evaluatedNot evaluatedStrict contracts pending reviewNo engine compatibility claim

Limitations and unknowns

  1. Tool evidence does not replace target-engine import, playback, contact, retargeting, masking, or visual review.
  2. Strict declared-contract failures are clip-level admission signals, not a blanket statement about authored quality.
  3. Candidate transforms remain external and require project acceptance before use.

Changes between AnimSmith versions

AnimSmith 0.10.0 — revalidated inventory, baseline, contracts, and bounded remediations with the official FBX-capable evaluator. AnimSmith 0.7.0 — historical evidence is retained only for history and is superseded for current-state conclusions.

Reproduction

Official release archive URL https://github.com/mmannerm/animsmith/releases/download/v0.10.0/animsmith-v0.10.0-x86_64-unknown-linux-gnu.tar.gz; archive SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith; evaluator binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387; tag v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f; working tree N/A; compiled features fbx/report. Expected command/help surface and representative FBX admission succeeded before the exhaustive serial batch. Current operation trace and scrubbed preflight are recorded at external:protofactor-melee-remediation-0.10.0/public-evidence-projection.json (SHA-256 4004862e2f2f7f3719e796c2cedb9d3c264eb943588d5cb2ae78baf917737ac7). No licensed asset, derivative, private path, or output file is published.

Sources

Animation pack evaluation: Protofactor Dual Swords Animset

Technical verdict: Usable with conditions

Evaluation completeness: partial — current mechanical and declared-contract evidence completed; engine, visual, contact, and artistic acceptance were not rerun.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Protofactor Dual Swords Animset evidence appendix

Technical decision

The current baseline is mechanically readable with AnimSmith 0.10.0, but this pack is not a blanket game-ready asset. 756 baseline commands completed successfully; constant-track notes and strict loop contracts require review. The 25 bounded remediation candidates comprise 24 gait-anchor and 1 prune-constant-tracks transform; all exited 0 with post-output checks recorded in the external evidence projection. They remain external, unpromoted candidates pending engine, contact, and visual review.

Capability coverage

Complete core

  • Source inventory and serial AnimSmith baseline completed without publishing licensed inputs or derivatives.
  • 189 delivered FBX candidates were inspected, measured, and linted in both output formats.
  • Declared contracts and bounded remediation trials were rerun with the current evaluator.

Partial supporting gameplay

  • 186 declared-contract inputs: 24 clean and 162 non-clean results in each output format.
  • Candidate transforms are mechanical evidence, not engine or artistic acceptance.

Absent

  • Current target-engine import/playback, blending, masking, retargeting, contact, performance, and artistic review.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=pack-local; construct project state graphs only from reviewed imported clips.
  2. Timing/synchronization: sync=contract-gated; use loop and seam findings to gate transitions.
  3. State ownership: owner=project-controller; assign root-motion ownership per state.
  4. Composition constraints: composition=full-body-review; validate masks, props, IK, and attachments.
  5. Acceptance gate: gate=engine-visual-contact; require import, playback, contact, and visual sign-off.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
PF-010majorDeclared loop and seam contracts do not pass for the full selected corpus; clip-level admission is required. Guidance.unknownKeep non-clean clips out of production runtime sets.Generic validation and conforming assistance may help.observed-animsmith; current contract evidence.
PF-011minorGenerated candidates are mechanical signals, not visual acceptance. Guidance.unknownReview deltas and playback.Generic track cleanup is applicable.observed-animsmith; 25/25 transforms exit 0.

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unitynot-evaluatedNo current import or playback was run.Disposable-project import and visual controller tests.
Unreal Enginenot-evaluatedNo current import or playback was run.Import, retarget, and blend tests.
Godotnot-evaluatedNo current import or playback was run.Conversion/import route and runtime tests.
Bevynot-evaluatedNo current import or playback was run.Conversion, loading, graph, and performance tests.

Fit and limitations

This is current mechanical evidence, not a guarantee of engine readiness, gameplay suitability, or artistic quality. Licensed source, candidate outputs, and local evidence remain private.

Changes between AnimSmith versions

AnimSmith 0.10.0 — revalidated current inventory, baseline, declared contracts, and bounded remediation using the official FBX-capable release binary. AnimSmith 0.7.0 — retained historical evidence is superseded for current-state conclusions.

Evidence status

See the readiness ladder and appendix. Mechanical completion is separate from engine and visual acceptance.

Sources

Animation pack evidence appendix: Protofactor Dual Swords Animset

Companion report: technical evaluation

Evidence status: partial — current source inventory, serial mechanical baseline, declared contracts, and bounded remediation completed; engine and artistic acceptance did not.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Current evidence boundary; the canonical readiness ladder remains authoritative.

Evaluation scope and provenance

FieldValue
Pack/editionLocal licensed constituent; revision unknown
Vendor/sourceProtofactor current product listing
Delivered scope189 FBX input candidates
Target useEngine use; no target controller supplied
Target enginesNot evaluated
Target rigs/packsNot evaluated
Source manifestExternal source inventory, SHA-256 6686db08c5d264823473332b0c35aa55e068e1d01595797bfc8710da6ea81a7e
Evaluation manifesturn:animsmith:skill:animation-pack-evaluation-manifest:1
Acquisition/license provenanceAuthorized local commercial input; no legal advice

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files189189Mechanical baseline completedN/A
Rigs/export variants188 animation-bearing candidatespartialMechanical inspection onlyRuntime compatibility not run
AnimSmith baseline189189756 baseline commands completed successfully; constant-track notes and strict loop contracts require review.N/A
Declared contracts186 selected motion inputs186186 declared-contract inputs: 24 clean and 162 non-clean results in each output format.Engine behavior is out of scope
Remediation candidates252524 gait-anchor + 1 prune-constant-tracks; all transforms exit 0External candidates remain unpromoted; engine/visual review pending
Engine import/playbackUnknown00Not run
Blend/mask/retargetUnknown00Not run

Claim legend

Uses observed-file, observed-animsmith, and not-evaluated.

Evaluation manifest and taxonomy

The current manifest follows urn:animsmith:skill:animation-pack-evaluation-manifest:1. File inventory is current; gameplay classification is not promoted without engine acceptance.

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00runtime classification withheld
continuous-locomotion00runtime classification withheld
locomotion-transition00runtime classification withheld
airborne00runtime classification withheld
traversal00runtime classification withheld
action-interaction00runtime classification withheld
reaction-death00runtime classification withheld
emote-cinematic00runtime classification withheld
other-unknown00runtime classification withheld
Total00Current runtime classification withheld

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquireevaluated-cleanExternal source retained
Preserve rawevaluated-cleanNew external evidence root
Inspectevaluated-cleanCurrent baseline completed
Segmentnot-evaluatedRuntime grouping not accepted
Root motionnot-evaluatedNo engine controller test
Conformpartially-evaluatedBounded candidate transforms only
Validatepartially-evaluatedBaseline and declared contracts complete
Optimizenot-evaluatedNo production optimization review
Exportnot-evaluatedNo target-engine export accepted
Gate/reportpartially-evaluatedEngine and artistic gates remain

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
All delivered candidatesMechanical baseline completeNot evaluatedNo current engine or artistic acceptance

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — vendor-intendedInventory and baseline completed.
Blended locomotionselected — evaluator-selected-generic-scenarioStrict contract findings require targeted review.
Root-motion controllernot-selectedNo controller was supplied.
State-machine transitionsselected — evaluator-selected-generic-scenarioContract evidence captured; engine validation pending.
Layered upper body/weaponsnot-selectedNo mask or attachment test.
Traversal/environmentnot-selectedNo target scene.
Contact actions/interactionsnot-selectedNo target contacts.
Retargeted/customizable charactersnot-selectedNo target rig.
Motion matching/searchnot-selectedNo target database.
Networked movementnot-selectedNo target networking design.
Runtime performancenot-selectedNo runtime benchmark.

Pack inventory and content evidence

Current inventory found 189 FBX candidates, including 188 animation-bearing candidates, 186 individual-motion candidates, and 1 combined-take candidates. This establishes source presence and mechanical coverage only.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
Baseline commands189/189Inputs are mechanically readable by the current evaluatorobserved-animsmith; 756 baseline commands completed successfully; constant-track notes and strict loop contracts require review.
Declared contracts186 selected inputsNon-clean strict contract results require clip-level admissionobserved-animsmith; 186 declared-contract inputs: 24 clean and 162 non-clean results in each output format.

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Mechanical cleanup and gait candidates24 gait-anchor + 1 prune-constant-tracks25/25 transform commands exit 0Post-output inspect/measure/diff/config-lint recorded in external:protofactor-melee-remediation-0.10.0/public-evidence-projection.json (SHA-256 4004862e2f2f7f3719e796c2cedb9d3c264eb943588d5cb2ae78baf917737ac7)External candidates remain unpromoted; no engine, visual, or artistic acceptance

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityNot evaluatedNo current importNot evaluatedImport and visual review
Unreal EngineNot evaluatedNo current importNot evaluatedImport and retarget review
GodotNot evaluatedNo current importNot evaluatedConversion and runtime review
BevyNot evaluatedNo current importNot evaluatedConversion and runtime review

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Delivered packMechanical inspection onlyNot evaluatedNot evaluatedStrict contracts pending reviewNo engine compatibility claim

Limitations and unknowns

  1. Tool evidence does not replace target-engine import, playback, contact, retargeting, masking, or visual review.
  2. Strict declared-contract failures are clip-level admission signals, not a blanket statement about authored quality.
  3. Candidate transforms remain external and require project acceptance before use.

Changes between AnimSmith versions

AnimSmith 0.10.0 — revalidated inventory, baseline, contracts, and bounded remediations with the official FBX-capable evaluator. AnimSmith 0.7.0 — historical evidence is retained only for history and is superseded for current-state conclusions.

Reproduction

Official release archive URL https://github.com/mmannerm/animsmith/releases/download/v0.10.0/animsmith-v0.10.0-x86_64-unknown-linux-gnu.tar.gz; archive SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith; evaluator binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387; tag v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f; working tree N/A; compiled features fbx/report. Expected command/help surface and representative FBX admission succeeded before the exhaustive serial batch. Current operation trace and scrubbed preflight are recorded at external:protofactor-melee-remediation-0.10.0/public-evidence-projection.json (SHA-256 4004862e2f2f7f3719e796c2cedb9d3c264eb943588d5cb2ae78baf917737ac7). No licensed asset, derivative, private path, or output file is published.

Sources

Animation pack evaluation: Protofactor Ultimate Animation Collection (partial: eight packs)

Technical verdict: Insufficient technical evidence

Evaluation completeness: partial — eight constituent source baselines and declared contracts were rerun with AnimSmith 0.10.0; no current collection binding, semantic classification, runtime-set, cross-pack, engine, or visual acceptance run exists.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: partial collection evidence appendix

Technical decision

The official AnimSmith 0.10.0 release reran Basic Locomotion, Sword & Shield, Campfire, Climbing, Injured, 1-Handed Melee, 2-Handed Melee, and Dual Swords. They contain 918 source candidates, including 895 individual motion-labelled inputs. This is current mechanical intake evidence, not collection-level approval.

No current collection binding was rendered, so this rollup has no accepted canonical logical-motion inventory, semantic roles, runtime sets, cross-pack handoffs, or package-wide remediation result. The eight constituents produced 159 external transform candidates, but none was promoted or accepted for an engine. A project must make its own clip-selection, root-authority, contact, controller, and target-character decisions.

Capability coverage

Complete core

  • Current source-inventory, baseline, and declared-contract evidence exists for each of the eight named constituents.
  • The current report establishes the mechanical scope boundary: 918 source candidates and 895 individual motion-labelled inputs.

Partial supporting gameplay

  • All eight constituents have bounded external AnimSmith transform candidates; their runtime selection, visual result, and gameplay suitability remain untested.

Absent

  • No current canonical role classification, runtime-set inventory, cross-pack blend/mask/transition evidence, target rig, engine run, or visual acceptance.
  • Fifteen collection constituents are outside this partial evaluation: 2-Handed Gun, Assault Rifle, Bazooka, Bow & Arrow, Combat Bare Fists, Creature, Crowd, Double Guns, Fencing, Hostage, Minigun, Push & Pull Cube, Shotgun, Wizard, and Zombie.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=not-evaluated; preserve constituent boundaries and declare selected clips before creating collection graphs.
  2. Timing/synchronization: sync=not-evaluated; resolve declared-contract findings and measure only project-selected transition or blend candidates.
  3. State ownership: owner=not-evaluated; assign one root/displacement authority per selected clip and state.
  4. Composition constraints: composition=full-body; do not approve masks, additive use, or cross-pack handoffs from this rollup.
  5. Acceptance gate: gate=current-cross-pack-engine-and-visual-review; rerun selected compatibility, controller, contact, retarget, build, and visual tests.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
UC-010blockerNo current collection binding, role taxonomy, or runtime-set inventory exists, so the corpus cannot safely define collection graphs. Readiness guidance applies.unknownSelect clips and declare collection roles, sets, and ownership in the target project.A current collection manifest could preserve declared grouping evidence.not-evaluated; 895 motion-labelled inputs remain unclassified at collection level.
UC-011majorCurrent declared-contract findings require clip-by-clip loop and continuity decisions before blend or transition use. Guidance: not applicable.artist-authorRepair or re-export malformed source where required; otherwise document intended policy.Declared mechanical diagnostics can support review, not infer intent.observed-animsmith; constituent-specific results are linked above.
UC-012majorCross-pack co-installation and engine behavior have no current validation. Guidance: not applicable.engine-configRun only project-relevant compatibility and engine tests using selected clips.Cross-pack diagnostics could make the comparison reproducible.not-evaluated; no current engine run.
UC-013majorFifteen constituents are excluded, so this partial rollup cannot support a collection-wide coverage, compatibility, or value conclusion. Guidance: not applicable.unknownEvaluate additional constituents in bounded waves with a defined game decision.Tooling cannot establish evidence for excluded content.not-evaluated; explicit scope boundary.

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unitynot-evaluatedNo current import or playback run.Co-import, controller, visual, root-motion, contacts, retarget, compression, and build tests.
Unreal Enginenot-evaluatedNo current import or playback run.Import, retarget, graph, contact, and build tests.
Godotnot-evaluatedNo current conversion, import, or playback run.Conversion/import, graph, contact, and export tests.
Bevynot-evaluatedNo current conversion, addressability, or runtime run.Conversion, target mapping, runtime, and performance tests.

Fit and limitations

This report is suitable only as a mechanically scoped intake record for a project prepared to select clips and run its own current compatibility and engine tests. It does not support a blanket game-ready, engine-ready, retarget-ready, or artistic-ready conclusion.

It is a poor basis for approving a universal locomotion graph, seamless cross-pack transitions, masks, contact actions, first-person, motion matching, networking, or the fifteen excluded constituents without new scoped evidence.

Changes between AnimSmith versions

AnimSmith 0.10.0 — official release evidence revalidated all eight constituent source baselines, declared contracts, and 159 bounded constituent remediation candidates. The current rollup intentionally records only constituent-derived counts and findings; collection-level taxonomy, runtime sets, and compatibility were not regenerated.

AnimSmith 0.7.0 — historical collection output recorded 582 logical motions, 90 runtime-set records, 14 cross-pack candidates, and 159 external candidates. Those superseded collection-derived claims are retained as historical context only and are not current evidence.

AnimSmith 0.4.0 — retained dated Unity 6000.5.8f1 graph probes recorded 22/22 contextual and 33/33 melee required checks with four expected Generic-rig failures; a shared-path comparison recorded 700 byte-identical comparisons across 28 constituent pairs. Neither result was rerun and neither is current engine or compatibility evidence.

Evidence status

Current evidence is the official 0.10.0 constituent rerun: 918 source candidates, 895 individual motion-labelled inputs, 3,672 baseline commands, and 159 external transform candidates across all eight constituents. Current canonical logical-motion and runtime-set totals are both zero because no collection classification or binding was accepted. The readiness ladder and appendix define the exact boundary.

Sources

Animation pack evidence appendix: Protofactor Ultimate Animation Collection (partial: eight packs)

Companion report: Protofactor Ultimate Animation Collection report

Evidence status: partial — current official 0.10.0 evidence is constituent-derived; collection binding, runtime, engine, and visual evidence are absent.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

This appendix preserves current constituent evidence. The canonical readiness ladder remains authoritative.

Evaluation manifest schema: urn:animsmith:skill:animation-pack-evaluation-manifest:1.

Evaluation scope and provenance

FieldValue
Pack/editionProtofactor Ultimate Animation Collection; partial evaluation of eight constituents
Vendor/sourceProtofactor Ultimate Animation Collection
Included scopeBasic Locomotion, Sword & Shield, Campfire, Climbing, Injured, 1-Handed Melee, 2-Handed Melee, and Dual Swords
Excluded scopeFifteen named constituents; no current inventory, classification, compatibility, or readiness conclusion for them
Delivered scopeAuthorized local commercial deliveries; 918 source candidates, including 895 individual motion-labelled inputs
Target useEngine-neutral, mechanically scoped collection intake
Target enginesNo current engine run
Source manifestsExternal scrubbed constituent inventories; digests and commands are recorded in the eight linked appendices
Evaluation manifestCurrent collection binding unavailable; no current collection output was rendered or accepted
Acquisition/license provenanceCommercial source was authorized locally; no transaction, redistribution, or licensing conclusion is made

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Source candidates918918constituent baseline completedCurrent mechanical evidence is constituent-derived
Individual motion-labelled inputs895895constituent declared-contract results recordedNo collection semantic classification was accepted
Canonical logical motionsunknown00No current collection binding or taxonomy output
Runtime setsunknown00No current runtime grouping was selected
AnimSmith baseline9189183,672 commands completedPer-constituent findings are linked below
AnimSmith transform candidates159159External outputs from all eight constituentsNo promotion, engine, visual, or artistic acceptance
Engine import/playback4 runtimes0 current0 currentNo current engine run selected

Claim legend

Current consequential claims use observed-file, observed-animsmith, documentation-stated, or not-evaluated as defined by the assessment taxonomy.

Evaluation manifest and taxonomy

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00Classification was not regenerated or accepted
continuous-locomotion00Classification was not regenerated or accepted
locomotion-transition00Classification was not regenerated or accepted
airborne00Classification was not regenerated or accepted
traversal00Classification was not regenerated or accepted
action-interaction00Classification was not regenerated or accepted
reaction-death00Classification was not regenerated or accepted
emote-cinematic00Classification was not regenerated or accepted
other-unknown00Classification was not regenerated or accepted
Total00918 source candidates and 895 motion-labelled inputs remain unclassified at collection level

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquirepartially-evaluatedAuthorized local inventories for eight constituents only
Preserve rawevaluated-cleanCommercial source was not modified
Inspectevaluated-findingCurrent constituent baseline completed for all source candidates
Segmentnot-evaluatedNo collection segmentation trial selected
Root motionnot-evaluatedNo collection root-motion policy or controller acceptance
Conformnot-evaluatedNo target rig, retarget trial, or conformance policy
Validateevaluated-findingCurrent constituent declared contracts recorded
Optimizepartially-evaluated159 external transform candidates; no collection promotion or acceptance
Exportnot-evaluatedNo runtime handoff selected
Gate/reportpartially-evaluatedCurrent constituent-derived boundary recorded

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
Eight-constituent source corpusevaluated-findingnot-evaluatednot-evaluated
Current collection roles or runtime setsnot-evaluatednot-evaluatednot-evaluated

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — evaluator-selected-generic-scenarioCurrent source inventory and constituent baseline captured
Blended locomotionnot-selectedSelect clips and run current cross-pack blend evidence
Root-motion controllernot-selectedDeclare root ownership and run a controller test
State-machine transitionsnot-selectedDeclare handoffs and run selected transition evidence
Layered upper body/weaponsnot-selectedDefine masks and test selected clips
Traversal/environmentnot-selectedDefine environment/contact decisions
Contact actions/interactionsnot-selectedDefine prop/contact decisions
Retargeted/customizable charactersnot-selectedSupply a target rig and retarget test
Motion matching/searchnot-selectedSupply a database and selection policy
Networked movementnot-selectedSupply an authority/rollback policy
Runtime performancenot-selectedSupply a runtime build and budget

Pack inventory and content evidence

ConstituentSource candidatesIndividual motion-labelled inputsCurrent evidence
Basic Locomotion179177baseline, declared contracts, and 39 external transform candidates
Sword & Shield136132baseline, declared contracts, and 28 external transform candidates
Campfire2925baseline, declared contracts, and 1 external transform candidate
Climbing7775baseline, declared contracts, and 1 external transform candidate
Injured7270baseline, declared contracts, and 15 external transform candidates
1-Handed Melee113110baseline, declared contracts, and 25 external transform candidates
2-Handed Melee123120baseline, declared contracts, and 25 external transform candidates
Dual Swords189186baseline, declared contracts, and 25 external transform candidates
Total918895Current evidence is constituent-derived

Names and packaging are not current semantic role proof. The fifteen excluded constituents are outside this inventory and were not used to infer collection capability.

Mechanical baseline

ConstituentCurrent baseline or declared-contract findingEvidence boundary
Basic Locomotion24,186 constant-track notes and 36 time-monotonic errors across 12 files; 58 pass and 119 fail per output formatobserved-animsmith
Sword & Shield544 baseline commands; 34 clean and 230 non-clean declared-contract resultsobserved-animsmith
Campfire3,664 constant-track notes across 27 animated files; 17 pass and 8 fail per output formatobserved-animsmith
Climbing9,011 constant-track notes; 34 pass and 41 fail per output formatobserved-animsmith
Injured9,915 constant-track notes; 28 pass and 42 fail per output formatobserved-animsmith
1-Handed Melee452 baseline commands; 23 clean and 87 non-clean declared-contract results per output formatobserved-animsmith
2-Handed Melee492 baseline commands and one duration warning; 13 clean and 107 non-clean declared-contract results per output formatobserved-animsmith
Dual Swords756 baseline commands; 24 clean and 162 non-clean declared-contract results per output formatobserved-animsmith

The aggregate baseline count is 3,672 commands. These are mechanical diagnostics, not an engine or artistic acceptance result.

AnimSmith remediation evidence

ScopeOperation/declarationsResultIndependent verificationRemaining caveat
CollectionNo collection transform or remediation runNo current collection output or promoted derivativeNone; no collection binding was renderedDo not infer cross-pack use from constituent candidates
Basic Locomotion12 slices, 24 gait anchors, and 3 constant-track prunes39/39 commands emitted candidatesPost-output inspect, measure, diff, and lint recorded externallyCandidate quality and engine use remain unaccepted
Campfire and ClimbingOne constant-track prune each2/2 commands emitted candidatesPost-output inspect, measure, diff, and lint recorded externallyCandidate quality and engine use remain unaccepted
Injured14 gait anchors and one constant-track prune15/15 commands emitted candidatesPost-output inspect, measure, diff, and lint recorded externallyCandidate quality and engine use remain unaccepted
Sword & ShieldBounded external transform declarations28 candidates; commands exited successfullyConstituents retain their exact command recordsCandidate quality and engine use remain unaccepted
1-Handed, 2-Handed, and Dual SwordsBounded external transform declarations25 candidates per constituent; commands exited successfullyConstituents retain their exact command recordsCandidate quality and engine use remain unaccepted

No commercial source or derivative is published by this report.

Engine procedures and evidence

RuntimeProcedure/evidenceResultBoundary
UnityNo current procedure selectednot-evaluatedImport, controller, visual, contacts, retarget, and build remain
Unreal EngineNo current procedure selectednot-evaluatedImport, retarget, graphs, contacts, and build remain
GodotNo current procedure selectednot-evaluatedConversion/import, graphs, contacts, and export remain
BevyNo current procedure selectednot-evaluatedConversion, target mapping, runtime, and performance remain

Rig, masking, and compatibility evidence

The current 0.10.0 constituent rerun did not produce a collection compatibility matrix, rig comparison, mask trial, retarget trial, or cross-pack transition trial. No current conclusion about shared skeletons, masks, props, root authority, contacts, or co-installation follows.

Limitations and unknowns

  • The report evaluates exactly eight constituents, not the full collection, and does not assess the fifteen excluded constituents.
  • Canonical roles, logical-motion totals, runtime sets, cross-pack handoffs, and collection-wide remedial outputs are absent in the current run.
  • No current engine import, playback, visual, target-character, retarget, contact, root-motion controller, compression, build, or performance evidence exists.
  • Current external candidates do not establish source modification, promotion, derivative delivery, license rights, or artistic acceptance.

Changes between AnimSmith versions

AnimSmith 0.10.0 — official release revalidated the eight constituent baselines, declared contracts, and 159 bounded remediation candidates; this appendix preserves their aggregate boundary without recreating historical collection taxonomy or runtime records.

AnimSmith 0.7.0 — the historical collection manifest, role taxonomy, runtime sets, cross-pack candidates, and collection remediation outputs are superseded. They are not current evidence.

AnimSmith 0.4.0 — retained dated shared-path evidence recorded 700 byte-identical comparisons across 28 constituent pairs, while Unity 6000.5.8f1 probes recorded 22/22 contextual and 33/33 melee graph checks with four expected Generic-rig failures. Neither result was rerun and neither is current compatibility or engine evidence.

Reproduction

The evaluator was the official Linux archive https://github.com/mmannerm/animsmith/releases/download/v0.10.0/animsmith-v0.10.0-x86_64-unknown-linux-gnu.tar.gz (SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662), member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith (SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387), tag v0.10.0, and peeled source commit db91d8dda3326f97f581d4d62104d928caec383f; working-tree state is N/A. The compiled feature surface was fbx,report. Version, top-level help, required inspect, measure, lint, report, and transform help, plus representative FBX admission, passed before the batches. The normalized preflight record is preflight-status-v1, SHA-256 739930cd9c04189be3ffe1d3f7381800898d8d4b35c75c9a519e57f6cfad1fad. Eight external, scrubbed constituent source inventories and command manifests are linked from the paired constituent appendices.

No fresh collection lint output is cited: no current collection binding was rendered or accepted. Reproduce a future collection claim in a fresh external evidence root, with the official binary, declared source inventory, and only the project-relevant cross-pack, engine, and visual controls.

Sources

Animation pack evaluation: Mixamo Basic Locomotion

Technical verdict: Insufficient technical evidence

Evaluation completeness: partial — file inspection does not establish license, runtime, visual, contact, retarget, or cross-pack behavior.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Evidence appendix

Technical decision

This constituent is one observed archive pair in the locally held collection. AnimSmith 0.10.0 mechanically inspected, measured, and linted all 12 extracted FBX files. The untouched baseline has 0 errors, 4 warnings, and 1,712 non-gating constant-track notes; these are file-ready evidence only, not gameplay or engine acceptance. The declared in-place XZ contract has 0 errors and 1 duration warning; the declared root-motion contract has 4 stationary-root errors and 3 duration warnings. No source bytes were changed. The readiness ladder remains the governing boundary.

Capability coverage

Complete core

  • Fresh source inventory, parsing, inspection, measurement, and baseline mechanical lint were completed for all delivered files with the declared current evaluator.

Partial supporting gameplay

  • The in-place XZ declaration has 0 errors but 1 duration warning; 4 root-motion-labelled files have stationary roots and that declaration has 3 duration warnings. Per-clip movement ownership is still not established.

Absent

  • No license terms, target engine import, blend graph, retargeting, contact, visual, performance, or production acceptance evidence was supplied.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=not-evaluated; do not construct a blend set from archive labels alone.
  2. Timing/synchronization: sync=not-evaluated; no per-member loop or phase contract was established.
  3. State ownership: owner=per-clip-declaration; set XZ, vertical, and yaw ownership only in the target project.
  4. Composition constraints: composition=full-body-default; do not promote masks, additive layers, or contacts without runtime evidence.
  5. Acceptance gate: gate=target-engine-visual-test; import, playback, blend, retarget, and visual/contact testing remain required.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
MIX-OWN-001moderateFour root-motion-labelled files have stationary roots under a package-wide animation-owned XZ declaration, so that policy can make characters run in place. Guidance: not applicable.engine-configDeclare ownership per clip after target-controller review.Not a generic rewrite; it needs semantic/project intent.observed-animsmith; current contract result

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unity unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and controller
Unreal Engine unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and retarget path
Godot unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and AnimationTree
Bevy unspecifiednot-evaluatedNo import or playback evidenceTest exact loader and graph

Fit and limitations

Best fit is a controlled evaluation intake where a project can supply per-clip intent and test its own controller. It is a poor fit for immediate drop-in compatibility claims. Cross-pack compatibility is unknown and must be tested against the exact intended constituents and target rig.

Changes between AnimSmith versions

AnimSmith 0.10.0 — Fresh inventory, baseline, declared-contract, offline-report, and FBX fix --dry-run passes used the official v0.10.0 artifact. The baseline remains error-free; the revalidated root-motion declaration finds 4 stationary-root errors. Output schema changed from v17 to v19 and measurements schema from v16 to v18. AnimSmith 0.7.0 evidence is retained historical evidence only.

Evidence status

12 delivered FBX files and 10 manifest-declared motions were evaluated mechanically with AnimSmith 0.10.0 at revision db91d8dda3326f97f581d4d62104d928caec383f. Fresh external inventories and scrubbed SHA-256 identities are retained outside the repository. The companion appendix records the schema and open evidence gates.

Sources

  • Delivered archive manifests and AnimSmith 0.10.0 output, retained externally as scrubbed evidence.

Animation pack evidence appendix: Mixamo Basic Locomotion

Companion report: Technical report

Evidence status: partial — source archives were evaluated externally; licensing, runtime, and visual evidence remain unavailable.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

This appendix records scrubbed evidence only. The canonical readiness ladder remains authoritative.

Evaluation scope and provenance

FieldValue
Pack/editionMixamo Basic Locomotion; local revision unknown
Vendor/sourceVendor identity observed in delivered metadata; listing URL not retained
Delivered scope12 extracted FBX files from paired archive variants
Target useEngine-neutral marketplace intake
Target enginesNot evaluated
Target rigs/packsOne declared character identity; cross-pack use not evaluated
Source manifestScrubbed SHA-256 retained externally
Evaluation manifestmixamo-basic-locomotion.manifest.json; SHA-256 65c359befba9cb1a73539bf0af73e575a4d1e9cf5e2085152940af7648ec94b0; schema urn:animsmith:skill:animation-pack-evaluation-manifest:1
Acquisition/license provenanceNo controlling license/readme/terms evidence was delivered; not legal advice

The evaluation manifest schema is urn:animsmith:skill:animation-pack-evaluation-manifest:1. The validated manifest models every delivered FBX as an opaque other-unknown unit because vendor records were not linked to archive members; 10 remains separate source metadata.

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files12120 errors; 4 baseline warningsSemantic file-to-manifest mapping incomplete
Rigs/export variants11Mixamo profile usedRetarget behavior unavailable
AnimSmith baseline12120 errors; 4 warnings; 1,712 notes—
Declared contracts12124 root-motion stationary-root errors; 1 in-place and 3 root-motion duration warningsPer-clip intent unavailable
AnimSmith visual reports200Generated externally but not visually inspected
Engine import/playback400No engines/project supplied
Blend/mask/retarget100No runtime contract supplied

Claim legend

Claims use observed-file, observed-animsmith, and not-evaluated as defined by the assessment taxonomy.

Evaluation manifest and taxonomy

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00Not classified
continuous-locomotion00Not classified
locomotion-transition00Not classified
airborne00Not classified
traversal00Not classified
action-interaction00Not classified
reaction-death00Not classified
emote-cinematic00Not classified
other-unknown1212One opaque unit per delivered FBX; no semantic classification asserted
Total1212Validated scrubbed external manifest

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquirepartially-evaluatedLocal archive identity recorded; license terms unavailable
Preserve rawevaluated-cleanArchives were not modified; extraction was external
Inspectevaluated-cleanAnimSmith 0.10.0 inspected every extracted FBX
Segmentnot-evaluatedManifest-to-member clip linkage was not established
Root motionpartially-evaluatedIn-place XZ contract has 0 errors and 1 duration warning; root-motion XZ contract has 4 stationary-root errors and 3 duration warnings; per-clip ownership remains open
Conformnot-evaluatedNo target contract authorized transforms
Validatepartially-evaluatedMechanical baseline and current variant contracts ran; engine validation deferred
Optimizenot-evaluatedNo optimization was authorized
Exportnot-evaluatedNo engine-facing export was evaluated
Gate/reportpartially-evaluatedThis scrubbed report retains the evidence boundary

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
Unclassified delivered corpusMechanical baseline completed with no errorsPer-clip intent, loops, and ownership incompleteEngine, retarget, contact, visual, and gameplay acceptance not evaluated

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — observed-pack-capabilityExternal inventory and baseline completed; license evidence remains unavailable
Blended locomotionselected — observed-pack-capabilityRuntime sets and engine blend tests not evaluated
Root-motion controllerselected — observed-pack-capabilityVariant-level ownership differs; choose per-clip policy in the target project
State-machine transitionsnot-selectedNo target state-machine contract supplied
Layered upper body/weaponsnot-selectedNo mask or weapon contract supplied
Traversal/environmentnot-selectedNo target traversal contract supplied
Contact actions/interactionsnot-selectedNo target contact contract supplied
Retargeted/customizable charactersnot-selectedNo target rig or retargeter supplied
Motion matching/searchnot-selectedNo search runtime contract supplied
Networked movementnot-selectedNo networked movement contract supplied
Runtime performancenot-selectedNo target hardware/runtime contract supplied

Pack inventory and content evidence

The external inventory contains 12 FBX files and 10 manifest-declared motions. Archive member counts exceed manifest motion counts by one non-motion/reference asset per archive. Delivered metadata declares 30 fps FBX export preferences, but this is not a target-engine import result.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
No mechanical errors12 filesFile-ready mechanical health onlyobserved-animsmith, AnimSmith 0.10.0
duration-sanity warnings4 findingsTwo no-track clips and two channel-end mismatches require semantic classification or reviewobserved-animsmith, AnimSmith 0.10.0
constant-track notesEntire corpusHygiene evidence, not authorization to rewriteobserved-animsmith

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Variant-level ownership mismatchFBX fix --dry-run representativeSafe refusal: fix accepts only glTF/GLBCaptured exit 2 and stderr under AnimSmith 0.10.0No conversion or transform was authorized

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityunspecifiedNot runnot-evaluatedExact import/controller test
Unreal EngineunspecifiedNot runnot-evaluatedExact import/retarget test
GodotunspecifiedNot runnot-evaluatedExact import/AnimationTree test
BevyunspecifiedNot runnot-evaluatedExact loader/graph test

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Delivered corpus / target projectMixamo profile resolved for lint onlyNot evaluatedArchive variant labels insufficient per clipNot evaluatedunknown

Limitations and unknowns

  1. License, target engine, project controller, runtime-set membership, source-to-manifest member mapping, retargeting, visual/contact quality, and cross-pack compatibility remain unverified.

Changes between AnimSmith versions

AnimSmith 0.10.0 — Official v0.10.0 rerun revalidated output schema v19 and measurements schema v18: 12/12 FBX admitted, baseline 0 errors, and the root-motion declaration found 4 stationary-root errors. AnimSmith 0.7.0 (output schema v17; measurements schema v16) is retained historical evidence only.

Reproduction

working-tree state is N/A (official release artifact).

Reproduction used the official release artifact: URL https://github.com/mmannerm/animsmith/releases/download/v0.10.0/animsmith-v0.10.0-x86_64-unknown-linux-gnu.tar.gz; archive SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith; binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387. Official tag is v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f; working-tree state N/A; compiled features fbx,report; output schema v19 and measurements schema v18.

Required evaluator checks succeeded for --version, top-level --help, inspect --help, measure --help, lint --help, and report --help, and representative FBX admission succeeded. Safe normalized preflight status is preflight-status-v1, SHA-256 739930cd9c04189be3ffe1d3f7381800898d8d4b35c75c9a519e57f6cfad1fad; available help digests are version 35bb24c48d2e2fcf9ae5753338f09722ee50f9c002d00df7d0d30afa5d2ac4a0, top-level 905f42d113783490bc8f3dea6bcda6bfc648a7e2f29b71fec99c41e6701099c8, inspect c45acaabc9e357af7c2c6265a18dccbb67b6cf1cb7dc45263652e0f5fa922c0b, measure cca0b44510c65ee671ac13b591712b6c67f6b1a4f2efdb0aca4ba26293e3d169, lint e216ccd638df639853687025f4eaab35e03c32d306b0dbcd0cb2c5c19ff53def, and report 8298b2c9358bc796362eae5aeae22d86c9a06843c0dec7eb0f06cee82b27ad03. External empty-baseline and variant-contract configs, inventories, command outputs, exit codes, and remaining digests are retained outside the repository.

Sources

  • Delivered metadata and external AnimSmith 0.10.0 evidence; no licensed payloads are published.

Animation pack evaluation: Mixamo Female Basic Locomotion

Technical verdict: Insufficient technical evidence

Evaluation completeness: partial — file inspection does not establish license, runtime, visual, contact, retarget, or cross-pack behavior.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Evidence appendix

Technical decision

This constituent is one observed archive pair in the locally held collection. AnimSmith 0.10.0 mechanically inspected, measured, and linted all 20 extracted FBX files. The untouched baseline has 0 errors, 5 warnings, and 2,843 non-gating constant-track notes; these are file-ready evidence only, not gameplay or engine acceptance. The declared in-place XZ contract has 0 errors and 2 duration warnings; the declared root-motion contract has 6 stationary-root errors and 3 duration warnings. No source bytes were changed. The readiness ladder remains the governing boundary.

Capability coverage

Complete core

  • Fresh source inventory, parsing, inspection, measurement, and baseline mechanical lint were completed for all delivered files with the declared current evaluator.

Partial supporting gameplay

  • The in-place XZ declaration has 0 errors but 2 duration warnings; 6 root-motion-labelled files have stationary roots and that declaration has 3 duration warnings. Per-clip movement ownership is still not established.

Absent

  • No license terms, target engine import, blend graph, retargeting, contact, visual, performance, or production acceptance evidence was supplied.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=not-evaluated; do not construct a blend set from archive labels alone.
  2. Timing/synchronization: sync=not-evaluated; no per-member loop or phase contract was established.
  3. State ownership: owner=per-clip-declaration; set XZ, vertical, and yaw ownership only in the target project.
  4. Composition constraints: composition=full-body-default; do not promote masks, additive layers, or contacts without runtime evidence.
  5. Acceptance gate: gate=target-engine-visual-test; import, playback, blend, retarget, and visual/contact testing remain required.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
MIX-OWN-001moderateSix root-motion-labelled files have stationary roots under a package-wide animation-owned XZ declaration, so that policy can make characters run in place. Guidance: not applicable.engine-configDeclare ownership per clip after target-controller review.Not a generic rewrite; it needs semantic/project intent.observed-animsmith; current contract result

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unity unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and controller
Unreal Engine unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and retarget path
Godot unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and AnimationTree
Bevy unspecifiednot-evaluatedNo import or playback evidenceTest exact loader and graph

Fit and limitations

Best fit is a controlled evaluation intake where a project can supply per-clip intent and test its own controller. It is a poor fit for immediate drop-in compatibility claims. Cross-pack compatibility is unknown and must be tested against the exact intended constituents and target rig.

Changes between AnimSmith versions

AnimSmith 0.10.0 — Fresh inventory, baseline, declared-contract, offline-report, and FBX fix --dry-run passes used the official v0.10.0 artifact. The baseline remains error-free; the revalidated root-motion declaration finds 6 stationary-root errors. Output schema changed from v17 to v19 and measurements schema from v16 to v18. AnimSmith 0.7.0 evidence is retained historical evidence only.

Evidence status

20 delivered FBX files and 18 manifest-declared motions were evaluated mechanically with AnimSmith 0.10.0 at revision db91d8dda3326f97f581d4d62104d928caec383f. Fresh external inventories and scrubbed SHA-256 identities are retained outside the repository. The companion appendix records the schema and open evidence gates.

Sources

  • Delivered archive manifests and AnimSmith 0.10.0 output, retained externally as scrubbed evidence.

Animation pack evidence appendix: Mixamo Female Basic Locomotion

Companion report: Technical report

Evidence status: partial — source archives were evaluated externally; licensing, runtime, and visual evidence remain unavailable.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

This appendix records scrubbed evidence only. The canonical readiness ladder remains authoritative.

Evaluation scope and provenance

FieldValue
Pack/editionMixamo Female Basic Locomotion; local revision unknown
Vendor/sourceVendor identity observed in delivered metadata; listing URL not retained
Delivered scope20 extracted FBX files from paired archive variants
Target useEngine-neutral marketplace intake
Target enginesNot evaluated
Target rigs/packsOne declared character identity; cross-pack use not evaluated
Source manifestScrubbed SHA-256 retained externally
Evaluation manifestmixamo-female-basic-locomotion.manifest.json; SHA-256 65abc6c475347a4af412e890432203405988ab147d7a99dd57dd6a5700833b0f; schema urn:animsmith:skill:animation-pack-evaluation-manifest:1
Acquisition/license provenanceNo controlling license/readme/terms evidence was delivered; not legal advice

The evaluation manifest schema is urn:animsmith:skill:animation-pack-evaluation-manifest:1. The validated manifest models every delivered FBX as an opaque other-unknown unit because vendor records were not linked to archive members; 18 remains separate source metadata.

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files20200 errors; 5 baseline warningsSemantic file-to-manifest mapping incomplete
Rigs/export variants11Mixamo profile usedRetarget behavior unavailable
AnimSmith baseline20200 errors; 5 warnings; 2,843 notes—
Declared contracts20206 root-motion stationary-root errors; 2 in-place and 3 root-motion duration warningsPer-clip intent unavailable
AnimSmith visual reports200Generated externally but not visually inspected
Engine import/playback400No engines/project supplied
Blend/mask/retarget100No runtime contract supplied

Claim legend

Claims use observed-file, observed-animsmith, and not-evaluated as defined by the assessment taxonomy.

Evaluation manifest and taxonomy

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00Not classified
continuous-locomotion00Not classified
locomotion-transition00Not classified
airborne00Not classified
traversal00Not classified
action-interaction00Not classified
reaction-death00Not classified
emote-cinematic00Not classified
other-unknown2020One opaque unit per delivered FBX; no semantic classification asserted
Total2020Validated scrubbed external manifest

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquirepartially-evaluatedLocal archive identity recorded; license terms unavailable
Preserve rawevaluated-cleanArchives were not modified; extraction was external
Inspectevaluated-cleanAnimSmith 0.10.0 inspected every extracted FBX
Segmentnot-evaluatedManifest-to-member clip linkage was not established
Root motionpartially-evaluatedIn-place XZ contract has 0 errors and 2 duration warnings; root-motion XZ contract has 6 stationary-root errors and 3 duration warnings; per-clip ownership remains open
Conformnot-evaluatedNo target contract authorized transforms
Validatepartially-evaluatedMechanical baseline and current variant contracts ran; engine validation deferred
Optimizenot-evaluatedNo optimization was authorized
Exportnot-evaluatedNo engine-facing export was evaluated
Gate/reportpartially-evaluatedThis scrubbed report retains the evidence boundary

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
Unclassified delivered corpusMechanical baseline completed with no errorsPer-clip intent, loops, and ownership incompleteEngine, retarget, contact, visual, and gameplay acceptance not evaluated

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — observed-pack-capabilityExternal inventory and baseline completed; license evidence remains unavailable
Blended locomotionselected — observed-pack-capabilityRuntime sets and engine blend tests not evaluated
Root-motion controllerselected — observed-pack-capabilityVariant-level ownership differs; choose per-clip policy in the target project
State-machine transitionsnot-selectedNo target state-machine contract supplied
Layered upper body/weaponsnot-selectedNo mask or weapon contract supplied
Traversal/environmentnot-selectedNo target traversal contract supplied
Contact actions/interactionsnot-selectedNo target contact contract supplied
Retargeted/customizable charactersnot-selectedNo target rig or retargeter supplied
Motion matching/searchnot-selectedNo search runtime contract supplied
Networked movementnot-selectedNo networked movement contract supplied
Runtime performancenot-selectedNo target hardware/runtime contract supplied

Pack inventory and content evidence

The external inventory contains 20 FBX files and 18 manifest-declared motions. Archive member counts exceed manifest motion counts by one non-motion/reference asset per archive. Delivered metadata declares 30 fps FBX export preferences, but this is not a target-engine import result.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
No mechanical errors20 filesFile-ready mechanical health onlyobserved-animsmith, AnimSmith 0.10.0
duration-sanity warnings5 findingsTwo no-track clips and three channel-end mismatches require semantic classification or reviewobserved-animsmith, AnimSmith 0.10.0
constant-track notesEntire corpusHygiene evidence, not authorization to rewriteobserved-animsmith

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Variant-level ownership mismatchFBX fix --dry-run representativeSafe refusal: fix accepts only glTF/GLBCaptured exit 2 and stderr under AnimSmith 0.10.0No conversion or transform was authorized

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityunspecifiedNot runnot-evaluatedExact import/controller test
Unreal EngineunspecifiedNot runnot-evaluatedExact import/retarget test
GodotunspecifiedNot runnot-evaluatedExact import/AnimationTree test
BevyunspecifiedNot runnot-evaluatedExact loader/graph test

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Delivered corpus / target projectMixamo profile resolved for lint onlyNot evaluatedArchive variant labels insufficient per clipNot evaluatedunknown

Limitations and unknowns

  1. License, target engine, project controller, runtime-set membership, source-to-manifest member mapping, retargeting, visual/contact quality, and cross-pack compatibility remain unverified.

Changes between AnimSmith versions

AnimSmith 0.10.0 — Official v0.10.0 rerun revalidated output schema v19 and measurements schema v18: 20/20 FBX admitted, baseline 0 errors, and the root-motion declaration found 6 stationary-root errors. AnimSmith 0.7.0 (output schema v17; measurements schema v16) is retained historical evidence only.

Reproduction

working-tree state is N/A (official release artifact).

Reproduction used the official release artifact: URL https://github.com/mmannerm/animsmith/releases/download/v0.10.0/animsmith-v0.10.0-x86_64-unknown-linux-gnu.tar.gz; archive SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith; binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387. Official tag is v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f; working-tree state N/A; compiled features fbx,report; output schema v19 and measurements schema v18.

Required evaluator checks succeeded for --version, top-level --help, inspect --help, measure --help, lint --help, and report --help, and representative FBX admission succeeded. Safe normalized preflight status is preflight-status-v1, SHA-256 739930cd9c04189be3ffe1d3f7381800898d8d4b35c75c9a519e57f6cfad1fad; available help digests are version 35bb24c48d2e2fcf9ae5753338f09722ee50f9c002d00df7d0d30afa5d2ac4a0, top-level 905f42d113783490bc8f3dea6bcda6bfc648a7e2f29b71fec99c41e6701099c8, inspect c45acaabc9e357af7c2c6265a18dccbb67b6cf1cb7dc45263652e0f5fa922c0b, measure cca0b44510c65ee671ac13b591712b6c67f6b1a4f2efdb0aca4ba26293e3d169, lint e216ccd638df639853687025f4eaab35e03c32d306b0dbcd0cb2c5c19ff53def, and report 8298b2c9358bc796362eae5aeae22d86c9a06843c0dec7eb0f06cee82b27ad03. External empty-baseline and variant-contract configs, inventories, command outputs, exit codes, and remaining digests are retained outside the repository.

Sources

  • Delivered metadata and external AnimSmith 0.10.0 evidence; no licensed payloads are published.

Animation pack evaluation: Mixamo Female Locomotion

Technical verdict: Insufficient technical evidence

Evaluation completeness: partial — file inspection does not establish license, runtime, visual, contact, retarget, or cross-pack behavior.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Evidence appendix

Technical decision

This constituent is one observed archive pair in the locally held collection. AnimSmith 0.10.0 mechanically inspected, measured, and linted all 18 extracted FBX files. The untouched baseline has 0 errors, 4 warnings, and 2,559 non-gating constant-track notes; these are file-ready evidence only, not gameplay or engine acceptance. The declared in-place XZ contract has 0 errors and 2 duration warnings; the declared root-motion contract has 4 stationary-root errors and 2 duration warnings. No source bytes were changed. The readiness ladder remains the governing boundary.

Capability coverage

Complete core

  • Fresh source inventory, parsing, inspection, measurement, and baseline mechanical lint were completed for all delivered files with the declared current evaluator.

Partial supporting gameplay

  • The in-place XZ declaration has 0 errors but 2 duration warnings; 4 root-motion-labelled files have stationary roots and that declaration has 2 duration warnings. Per-clip movement ownership is still not established.

Absent

  • No license terms, target engine import, blend graph, retargeting, contact, visual, performance, or production acceptance evidence was supplied.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=not-evaluated; do not construct a blend set from archive labels alone.
  2. Timing/synchronization: sync=not-evaluated; no per-member loop or phase contract was established.
  3. State ownership: owner=per-clip-declaration; set XZ, vertical, and yaw ownership only in the target project.
  4. Composition constraints: composition=full-body-default; do not promote masks, additive layers, or contacts without runtime evidence.
  5. Acceptance gate: gate=target-engine-visual-test; import, playback, blend, retarget, and visual/contact testing remain required.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
MIX-OWN-001moderateFour root-motion-labelled files have stationary roots under a package-wide animation-owned XZ declaration, so that policy can make characters run in place. Guidance: not applicable.engine-configDeclare ownership per clip after target-controller review.Not a generic rewrite; it needs semantic/project intent.observed-animsmith; current contract result

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unity unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and controller
Unreal Engine unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and retarget path
Godot unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and AnimationTree
Bevy unspecifiednot-evaluatedNo import or playback evidenceTest exact loader and graph

Fit and limitations

Best fit is a controlled evaluation intake where a project can supply per-clip intent and test its own controller. It is a poor fit for immediate drop-in compatibility claims. Cross-pack compatibility is unknown and must be tested against the exact intended constituents and target rig.

Changes between AnimSmith versions

AnimSmith 0.10.0 — Fresh inventory, baseline, declared-contract, offline-report, and FBX fix --dry-run passes used the official v0.10.0 artifact. The baseline remains error-free; the revalidated root-motion declaration finds 4 stationary-root errors. Output schema changed from v17 to v19 and measurements schema from v16 to v18. AnimSmith 0.7.0 evidence is retained historical evidence only.

Evidence status

18 delivered FBX files and 16 manifest-declared motions were evaluated mechanically with AnimSmith 0.10.0 at revision db91d8dda3326f97f581d4d62104d928caec383f. Fresh external inventories and scrubbed SHA-256 identities are retained outside the repository. The companion appendix records the schema and open evidence gates.

Sources

  • Delivered archive manifests and AnimSmith 0.10.0 output, retained externally as scrubbed evidence.

Animation pack evidence appendix: Mixamo Female Locomotion

Companion report: Technical report

Evidence status: partial — source archives were evaluated externally; licensing, runtime, and visual evidence remain unavailable.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

This appendix records scrubbed evidence only. The canonical readiness ladder remains authoritative.

Evaluation scope and provenance

FieldValue
Pack/editionMixamo Female Locomotion; local revision unknown
Vendor/sourceVendor identity observed in delivered metadata; listing URL not retained
Delivered scope18 extracted FBX files from paired archive variants
Target useEngine-neutral marketplace intake
Target enginesNot evaluated
Target rigs/packsOne declared character identity; cross-pack use not evaluated
Source manifestScrubbed SHA-256 retained externally
Evaluation manifestmixamo-female-locomotion.manifest.json; SHA-256 548ee8c16b12a5564d0f422a51dc35610a7721b817d0cdb5c4e08c1f38350716; schema urn:animsmith:skill:animation-pack-evaluation-manifest:1
Acquisition/license provenanceNo controlling license/readme/terms evidence was delivered; not legal advice

The evaluation manifest schema is urn:animsmith:skill:animation-pack-evaluation-manifest:1. The validated manifest models every delivered FBX as an opaque other-unknown unit because vendor records were not linked to archive members; 16 remains separate source metadata.

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files18180 errors; 4 baseline warningsSemantic file-to-manifest mapping incomplete
Rigs/export variants11Mixamo profile usedRetarget behavior unavailable
AnimSmith baseline18180 errors; 4 warnings; 2,559 notes—
Declared contracts18184 root-motion stationary-root errors; 2 in-place and 2 root-motion duration warningsPer-clip intent unavailable
AnimSmith visual reports200Generated externally but not visually inspected
Engine import/playback400No engines/project supplied
Blend/mask/retarget100No runtime contract supplied

Claim legend

Claims use observed-file, observed-animsmith, and not-evaluated as defined by the assessment taxonomy.

Evaluation manifest and taxonomy

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00Not classified
continuous-locomotion00Not classified
locomotion-transition00Not classified
airborne00Not classified
traversal00Not classified
action-interaction00Not classified
reaction-death00Not classified
emote-cinematic00Not classified
other-unknown1818One opaque unit per delivered FBX; no semantic classification asserted
Total1818Validated scrubbed external manifest

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquirepartially-evaluatedLocal archive identity recorded; license terms unavailable
Preserve rawevaluated-cleanArchives were not modified; extraction was external
Inspectevaluated-cleanAnimSmith 0.10.0 inspected every extracted FBX
Segmentnot-evaluatedManifest-to-member clip linkage was not established
Root motionpartially-evaluatedIn-place XZ contract has 0 errors and 2 duration warnings; root-motion XZ contract has 4 stationary-root errors and 2 duration warnings; per-clip ownership remains open
Conformnot-evaluatedNo target contract authorized transforms
Validatepartially-evaluatedMechanical baseline and current variant contracts ran; engine validation deferred
Optimizenot-evaluatedNo optimization was authorized
Exportnot-evaluatedNo engine-facing export was evaluated
Gate/reportpartially-evaluatedThis scrubbed report retains the evidence boundary

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
Unclassified delivered corpusMechanical baseline completed with no errorsPer-clip intent, loops, and ownership incompleteEngine, retarget, contact, visual, and gameplay acceptance not evaluated

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — observed-pack-capabilityExternal inventory and baseline completed; license evidence remains unavailable
Blended locomotionselected — observed-pack-capabilityRuntime sets and engine blend tests not evaluated
Root-motion controllerselected — observed-pack-capabilityVariant-level ownership differs; choose per-clip policy in the target project
State-machine transitionsnot-selectedNo target state-machine contract supplied
Layered upper body/weaponsnot-selectedNo mask or weapon contract supplied
Traversal/environmentnot-selectedNo target traversal contract supplied
Contact actions/interactionsnot-selectedNo target contact contract supplied
Retargeted/customizable charactersnot-selectedNo target rig or retargeter supplied
Motion matching/searchnot-selectedNo search runtime contract supplied
Networked movementnot-selectedNo networked movement contract supplied
Runtime performancenot-selectedNo target hardware/runtime contract supplied

Pack inventory and content evidence

The external inventory contains 18 FBX files and 16 manifest-declared motions. Archive member counts exceed manifest motion counts by one non-motion/reference asset per archive. Delivered metadata declares 30 fps FBX export preferences, but this is not a target-engine import result.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
No mechanical errors18 filesFile-ready mechanical health onlyobserved-animsmith, AnimSmith 0.10.0
duration-sanity warnings4 findingsTwo no-track clips and two channel-end mismatches require semantic classification or reviewobserved-animsmith, AnimSmith 0.10.0
constant-track notesEntire corpusHygiene evidence, not authorization to rewriteobserved-animsmith

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Variant-level ownership mismatchFBX fix --dry-run representativeSafe refusal: fix accepts only glTF/GLBCaptured exit 2 and stderr under AnimSmith 0.10.0No conversion or transform was authorized

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityunspecifiedNot runnot-evaluatedExact import/controller test
Unreal EngineunspecifiedNot runnot-evaluatedExact import/retarget test
GodotunspecifiedNot runnot-evaluatedExact import/AnimationTree test
BevyunspecifiedNot runnot-evaluatedExact loader/graph test

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Delivered corpus / target projectMixamo profile resolved for lint onlyNot evaluatedArchive variant labels insufficient per clipNot evaluatedunknown

Limitations and unknowns

  1. License, target engine, project controller, runtime-set membership, source-to-manifest member mapping, retargeting, visual/contact quality, and cross-pack compatibility remain unverified.

Changes between AnimSmith versions

AnimSmith 0.10.0 — Official v0.10.0 rerun revalidated output schema v19 and measurements schema v18: 18/18 FBX admitted, baseline 0 errors, and the root-motion declaration found 4 stationary-root errors. AnimSmith 0.7.0 (output schema v17; measurements schema v16) is retained historical evidence only.

Reproduction

working-tree state is N/A (official release artifact).

Reproduction used the official release artifact: URL https://github.com/mmannerm/animsmith/releases/download/v0.10.0/animsmith-v0.10.0-x86_64-unknown-linux-gnu.tar.gz; archive SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith; binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387. Official tag is v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f; working-tree state N/A; compiled features fbx,report; output schema v19 and measurements schema v18.

Required evaluator checks succeeded for --version, top-level --help, inspect --help, measure --help, lint --help, and report --help, and representative FBX admission succeeded. Safe normalized preflight status is preflight-status-v1, SHA-256 739930cd9c04189be3ffe1d3f7381800898d8d4b35c75c9a519e57f6cfad1fad; available help digests are version 35bb24c48d2e2fcf9ae5753338f09722ee50f9c002d00df7d0d30afa5d2ac4a0, top-level 905f42d113783490bc8f3dea6bcda6bfc648a7e2f29b71fec99c41e6701099c8, inspect c45acaabc9e357af7c2c6265a18dccbb67b6cf1cb7dc45263652e0f5fa922c0b, measure cca0b44510c65ee671ac13b591712b6c67f6b1a4f2efdb0aca4ba26293e3d169, lint e216ccd638df639853687025f4eaab35e03c32d306b0dbcd0cb2c5c19ff53def, and report 8298b2c9358bc796362eae5aeae22d86c9a06843c0dec7eb0f06cee82b27ad03. External empty-baseline and variant-contract configs, inventories, command outputs, exit codes, and remaining digests are retained outside the repository.

Sources

  • Delivered metadata and external AnimSmith 0.10.0 evidence; no licensed payloads are published.

Animation pack evaluation: Mixamo Locomotion

Technical verdict: Insufficient technical evidence

Evaluation completeness: partial — file inspection does not establish license, runtime, visual, contact, retarget, or cross-pack behavior.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Evidence appendix

Technical decision

This constituent is one observed archive pair in the locally held collection. AnimSmith 0.10.0 mechanically inspected, measured, and linted all 20 extracted FBX files. The untouched baseline has 0 errors, 5 warnings, and 2,852 non-gating constant-track notes; these are file-ready evidence only, not gameplay or engine acceptance. The declared in-place XZ contract has 0 errors and 1 duration warning; the declared root-motion contract has 6 stationary-root errors and 4 duration warnings. No source bytes were changed. The readiness ladder remains the governing boundary.

Capability coverage

Complete core

  • Fresh source inventory, parsing, inspection, measurement, and baseline mechanical lint were completed for all delivered files with the declared current evaluator.

Partial supporting gameplay

  • The in-place XZ declaration has 0 errors but 1 duration warning; 6 root-motion-labelled files have stationary roots and that declaration has 4 duration warnings. Per-clip movement ownership is still not established.

Absent

  • No license terms, target engine import, blend graph, retargeting, contact, visual, performance, or production acceptance evidence was supplied.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=not-evaluated; do not construct a blend set from archive labels alone.
  2. Timing/synchronization: sync=not-evaluated; no per-member loop or phase contract was established.
  3. State ownership: owner=per-clip-declaration; set XZ, vertical, and yaw ownership only in the target project.
  4. Composition constraints: composition=full-body-default; do not promote masks, additive layers, or contacts without runtime evidence.
  5. Acceptance gate: gate=target-engine-visual-test; import, playback, blend, retarget, and visual/contact testing remain required.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
MIX-OWN-001moderateSix root-motion-labelled files have stationary roots under a package-wide animation-owned XZ declaration, so that policy can make characters run in place. Guidance: not applicable.engine-configDeclare ownership per clip after target-controller review.Not a generic rewrite; it needs semantic/project intent.observed-animsmith; current contract result

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unity unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and controller
Unreal Engine unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and retarget path
Godot unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and AnimationTree
Bevy unspecifiednot-evaluatedNo import or playback evidenceTest exact loader and graph

Fit and limitations

Best fit is a controlled evaluation intake where a project can supply per-clip intent and test its own controller. It is a poor fit for immediate drop-in compatibility claims. Cross-pack compatibility is unknown and must be tested against the exact intended constituents and target rig.

Changes between AnimSmith versions

AnimSmith 0.10.0 — Fresh inventory, baseline, declared-contract, offline-report, and FBX fix --dry-run passes used the official v0.10.0 artifact. The baseline remains error-free; the revalidated root-motion declaration finds 6 stationary-root errors. Output schema changed from v17 to v19 and measurements schema from v16 to v18. AnimSmith 0.7.0 evidence is retained historical evidence only.

Evidence status

20 delivered FBX files and 18 manifest-declared motions were evaluated mechanically with AnimSmith 0.10.0 at revision db91d8dda3326f97f581d4d62104d928caec383f. Fresh external inventories and scrubbed SHA-256 identities are retained outside the repository. The companion appendix records the schema and open evidence gates.

Sources

  • Delivered archive manifests and AnimSmith 0.10.0 output, retained externally as scrubbed evidence.

Animation pack evidence appendix: Mixamo Locomotion

Companion report: Technical report

Evidence status: partial — source archives were evaluated externally; licensing, runtime, and visual evidence remain unavailable.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

This appendix records scrubbed evidence only. The canonical readiness ladder remains authoritative.

Evaluation scope and provenance

FieldValue
Pack/editionMixamo Locomotion; local revision unknown
Vendor/sourceVendor identity observed in delivered metadata; listing URL not retained
Delivered scope20 extracted FBX files from paired archive variants
Target useEngine-neutral marketplace intake
Target enginesNot evaluated
Target rigs/packsOne declared character identity; cross-pack use not evaluated
Source manifestScrubbed SHA-256 retained externally
Evaluation manifestmixamo-locomotion.manifest.json; SHA-256 fd7c3927f33234a15ef65784cc08bccf248991c28153601ada44eb10619db0e3; schema urn:animsmith:skill:animation-pack-evaluation-manifest:1
Acquisition/license provenanceNo controlling license/readme/terms evidence was delivered; not legal advice

The evaluation manifest schema is urn:animsmith:skill:animation-pack-evaluation-manifest:1. The validated manifest models every delivered FBX as an opaque other-unknown unit because vendor records were not linked to archive members; 18 remains separate source metadata.

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files20200 errors; 5 baseline warningsSemantic file-to-manifest mapping incomplete
Rigs/export variants11Mixamo profile usedRetarget behavior unavailable
AnimSmith baseline20200 errors; 5 warnings; 2,852 notes—
Declared contracts20206 root-motion stationary-root errors; 1 in-place and 4 root-motion duration warningsPer-clip intent unavailable
AnimSmith visual reports200Generated externally but not visually inspected
Engine import/playback400No engines/project supplied
Blend/mask/retarget100No runtime contract supplied

Claim legend

Claims use observed-file, observed-animsmith, and not-evaluated as defined by the assessment taxonomy.

Evaluation manifest and taxonomy

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00Not classified
continuous-locomotion00Not classified
locomotion-transition00Not classified
airborne00Not classified
traversal00Not classified
action-interaction00Not classified
reaction-death00Not classified
emote-cinematic00Not classified
other-unknown2020One opaque unit per delivered FBX; no semantic classification asserted
Total2020Validated scrubbed external manifest

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquirepartially-evaluatedLocal archive identity recorded; license terms unavailable
Preserve rawevaluated-cleanArchives were not modified; extraction was external
Inspectevaluated-cleanAnimSmith 0.10.0 inspected every extracted FBX
Segmentnot-evaluatedManifest-to-member clip linkage was not established
Root motionpartially-evaluatedIn-place XZ contract has 0 errors and 1 duration warning; root-motion XZ contract has 6 stationary-root errors and 4 duration warnings; per-clip ownership remains open
Conformnot-evaluatedNo target contract authorized transforms
Validatepartially-evaluatedMechanical baseline and current variant contracts ran; engine validation deferred
Optimizenot-evaluatedNo optimization was authorized
Exportnot-evaluatedNo engine-facing export was evaluated
Gate/reportpartially-evaluatedThis scrubbed report retains the evidence boundary

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
Unclassified delivered corpusMechanical baseline completed with no errorsPer-clip intent, loops, and ownership incompleteEngine, retarget, contact, visual, and gameplay acceptance not evaluated

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — observed-pack-capabilityExternal inventory and baseline completed; license evidence remains unavailable
Blended locomotionselected — observed-pack-capabilityRuntime sets and engine blend tests not evaluated
Root-motion controllerselected — observed-pack-capabilityVariant-level ownership differs; choose per-clip policy in the target project
State-machine transitionsnot-selectedNo target state-machine contract supplied
Layered upper body/weaponsnot-selectedNo mask or weapon contract supplied
Traversal/environmentnot-selectedNo target traversal contract supplied
Contact actions/interactionsnot-selectedNo target contact contract supplied
Retargeted/customizable charactersnot-selectedNo target rig or retargeter supplied
Motion matching/searchnot-selectedNo search runtime contract supplied
Networked movementnot-selectedNo networked movement contract supplied
Runtime performancenot-selectedNo target hardware/runtime contract supplied

Pack inventory and content evidence

The external inventory contains 20 FBX files and 18 manifest-declared motions. Archive member counts exceed manifest motion counts by one non-motion/reference asset per archive. Delivered metadata declares 30 fps FBX export preferences, but this is not a target-engine import result.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
No mechanical errors20 filesFile-ready mechanical health onlyobserved-animsmith, AnimSmith 0.10.0
duration-sanity warnings5 findingsTwo no-track clips and three channel-end mismatches require semantic classification or reviewobserved-animsmith, AnimSmith 0.10.0
constant-track notesEntire corpusHygiene evidence, not authorization to rewriteobserved-animsmith

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Variant-level ownership mismatchFBX fix --dry-run representativeSafe refusal: fix accepts only glTF/GLBCaptured exit 2 and stderr under AnimSmith 0.10.0No conversion or transform was authorized

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityunspecifiedNot runnot-evaluatedExact import/controller test
Unreal EngineunspecifiedNot runnot-evaluatedExact import/retarget test
GodotunspecifiedNot runnot-evaluatedExact import/AnimationTree test
BevyunspecifiedNot runnot-evaluatedExact loader/graph test

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Delivered corpus / target projectMixamo profile resolved for lint onlyNot evaluatedArchive variant labels insufficient per clipNot evaluatedunknown

Limitations and unknowns

  1. License, target engine, project controller, runtime-set membership, source-to-manifest member mapping, retargeting, visual/contact quality, and cross-pack compatibility remain unverified.

Changes between AnimSmith versions

AnimSmith 0.10.0 — Official v0.10.0 rerun revalidated output schema v19 and measurements schema v18: 20/20 FBX admitted, baseline 0 errors, and the root-motion declaration found 6 stationary-root errors. AnimSmith 0.7.0 (output schema v17; measurements schema v16) is retained historical evidence only.

Reproduction

working-tree state is N/A (official release artifact).

Reproduction used the official release artifact: URL https://github.com/mmannerm/animsmith/releases/download/v0.10.0/animsmith-v0.10.0-x86_64-unknown-linux-gnu.tar.gz; archive SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith; binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387. Official tag is v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f; working-tree state N/A; compiled features fbx,report; output schema v19 and measurements schema v18.

Required evaluator checks succeeded for --version, top-level --help, inspect --help, measure --help, lint --help, and report --help, and representative FBX admission succeeded. Safe normalized preflight status is preflight-status-v1, SHA-256 739930cd9c04189be3ffe1d3f7381800898d8d4b35c75c9a519e57f6cfad1fad; available help digests are version 35bb24c48d2e2fcf9ae5753338f09722ee50f9c002d00df7d0d30afa5d2ac4a0, top-level 905f42d113783490bc8f3dea6bcda6bfc648a7e2f29b71fec99c41e6701099c8, inspect c45acaabc9e357af7c2c6265a18dccbb67b6cf1cb7dc45263652e0f5fa922c0b, measure cca0b44510c65ee671ac13b591712b6c67f6b1a4f2efdb0aca4ba26293e3d169, lint e216ccd638df639853687025f4eaab35e03c32d306b0dbcd0cb2c5c19ff53def, and report 8298b2c9358bc796362eae5aeae22d86c9a06843c0dec7eb0f06cee82b27ad03. External empty-baseline and variant-contract configs, inventories, command outputs, exit codes, and remaining digests are retained outside the repository.

Sources

  • Delivered metadata and external AnimSmith 0.10.0 evidence; no licensed payloads are published.

Animation pack evaluation: Mixamo Longbow Locomotion

Technical verdict: Insufficient technical evidence

Evaluation completeness: partial — file inspection does not establish license, runtime, visual, contact, retarget, or cross-pack behavior.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Evidence appendix

Technical decision

This constituent is one observed archive pair in the locally held collection. AnimSmith 0.10.0 mechanically inspected, measured, and linted all 22 extracted FBX files. The untouched baseline has 0 errors, 3 warnings, and 3,126 non-gating constant-track notes; these are file-ready evidence only, not gameplay or engine acceptance. The declared in-place XZ contract has 0 errors and 1 duration warning; the declared root-motion contract has 3 stationary-root errors and 2 duration warnings. No source bytes were changed. The readiness ladder remains the governing boundary.

Capability coverage

Complete core

  • Fresh source inventory, parsing, inspection, measurement, and baseline mechanical lint were completed for all delivered files with the declared current evaluator.

Partial supporting gameplay

  • The in-place XZ declaration has 0 errors but 1 duration warning; 3 root-motion-labelled files have stationary roots and that declaration has 2 duration warnings. Per-clip movement ownership is still not established.

Absent

  • No license terms, target engine import, blend graph, retargeting, contact, visual, performance, or production acceptance evidence was supplied.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=not-evaluated; do not construct a blend set from archive labels alone.
  2. Timing/synchronization: sync=not-evaluated; no per-member loop or phase contract was established.
  3. State ownership: owner=per-clip-declaration; set XZ, vertical, and yaw ownership only in the target project.
  4. Composition constraints: composition=full-body-default; do not promote masks, additive layers, or contacts without runtime evidence.
  5. Acceptance gate: gate=target-engine-visual-test; import, playback, blend, retarget, and visual/contact testing remain required.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
MIX-OWN-001moderateThree root-motion-labelled files have stationary roots under a package-wide animation-owned XZ declaration, so that policy can make characters run in place. Guidance: not applicable.engine-configDeclare ownership per clip after target-controller review.Not a generic rewrite; it needs semantic/project intent.observed-animsmith; current contract result

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unity unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and controller
Unreal Engine unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and retarget path
Godot unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and AnimationTree
Bevy unspecifiednot-evaluatedNo import or playback evidenceTest exact loader and graph

Fit and limitations

Best fit is a controlled evaluation intake where a project can supply per-clip intent and test its own controller. It is a poor fit for immediate drop-in compatibility claims. Cross-pack compatibility is unknown and must be tested against the exact intended constituents and target rig.

Changes between AnimSmith versions

AnimSmith 0.10.0 — Fresh inventory, baseline, declared-contract, offline-report, and FBX fix --dry-run passes used the official v0.10.0 artifact. The baseline remains error-free; the revalidated root-motion declaration finds 3 stationary-root errors. Output schema changed from v17 to v19 and measurements schema from v16 to v18. AnimSmith 0.7.0 evidence is retained historical evidence only.

Evidence status

22 delivered FBX files and 20 manifest-declared motions were evaluated mechanically with AnimSmith 0.10.0 at revision db91d8dda3326f97f581d4d62104d928caec383f. Fresh external inventories and scrubbed SHA-256 identities are retained outside the repository. The companion appendix records the schema and open evidence gates.

Sources

  • Delivered archive manifests and AnimSmith 0.10.0 output, retained externally as scrubbed evidence.

Animation pack evidence appendix: Mixamo Longbow Locomotion

Companion report: Technical report

Evidence status: partial — source archives were evaluated externally; licensing, runtime, and visual evidence remain unavailable.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

This appendix records scrubbed evidence only. The canonical readiness ladder remains authoritative.

Evaluation scope and provenance

FieldValue
Pack/editionMixamo Longbow Locomotion; local revision unknown
Vendor/sourceVendor identity observed in delivered metadata; listing URL not retained
Delivered scope22 extracted FBX files from paired archive variants
Target useEngine-neutral marketplace intake
Target enginesNot evaluated
Target rigs/packsOne declared character identity; cross-pack use not evaluated
Source manifestScrubbed SHA-256 retained externally
Evaluation manifestmixamo-longbow-locomotion.manifest.json; SHA-256 39e8baadfd586d57d71d4e5831051d9cb3036af4540b8c8bc7e7646e111c1b5f; schema urn:animsmith:skill:animation-pack-evaluation-manifest:1
Acquisition/license provenanceNo controlling license/readme/terms evidence was delivered; not legal advice

The evaluation manifest schema is urn:animsmith:skill:animation-pack-evaluation-manifest:1. The validated manifest models every delivered FBX as an opaque other-unknown unit because vendor records were not linked to archive members; 20 remains separate source metadata.

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files22220 errors; 3 baseline warningsSemantic file-to-manifest mapping incomplete
Rigs/export variants11Mixamo profile usedRetarget behavior unavailable
AnimSmith baseline22220 errors; 3 warnings; 3,126 notes—
Declared contracts22223 root-motion stationary-root errors; 1 in-place and 2 root-motion duration warningsPer-clip intent unavailable
AnimSmith visual reports200Generated externally but not visually inspected
Engine import/playback400No engines/project supplied
Blend/mask/retarget100No runtime contract supplied

Claim legend

Claims use observed-file, observed-animsmith, and not-evaluated as defined by the assessment taxonomy.

Evaluation manifest and taxonomy

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00Not classified
continuous-locomotion00Not classified
locomotion-transition00Not classified
airborne00Not classified
traversal00Not classified
action-interaction00Not classified
reaction-death00Not classified
emote-cinematic00Not classified
other-unknown2222One opaque unit per delivered FBX; no semantic classification asserted
Total2222Validated scrubbed external manifest

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquirepartially-evaluatedLocal archive identity recorded; license terms unavailable
Preserve rawevaluated-cleanArchives were not modified; extraction was external
Inspectevaluated-cleanAnimSmith 0.10.0 inspected every extracted FBX
Segmentnot-evaluatedManifest-to-member clip linkage was not established
Root motionpartially-evaluatedIn-place XZ contract has 0 errors and 1 duration warning; root-motion XZ contract has 3 stationary-root errors and 2 duration warnings; per-clip ownership remains open
Conformnot-evaluatedNo target contract authorized transforms
Validatepartially-evaluatedMechanical baseline and current variant contracts ran; engine validation deferred
Optimizenot-evaluatedNo optimization was authorized
Exportnot-evaluatedNo engine-facing export was evaluated
Gate/reportpartially-evaluatedThis scrubbed report retains the evidence boundary

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
Unclassified delivered corpusMechanical baseline completed with no errorsPer-clip intent, loops, and ownership incompleteEngine, retarget, contact, visual, and gameplay acceptance not evaluated

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — observed-pack-capabilityExternal inventory and baseline completed; license evidence remains unavailable
Blended locomotionselected — observed-pack-capabilityRuntime sets and engine blend tests not evaluated
Root-motion controllerselected — observed-pack-capabilityVariant-level ownership differs; choose per-clip policy in the target project
State-machine transitionsnot-selectedNo target state-machine contract supplied
Layered upper body/weaponsnot-selectedNo mask or weapon contract supplied
Traversal/environmentnot-selectedNo target traversal contract supplied
Contact actions/interactionsnot-selectedNo target contact contract supplied
Retargeted/customizable charactersnot-selectedNo target rig or retargeter supplied
Motion matching/searchnot-selectedNo search runtime contract supplied
Networked movementnot-selectedNo networked movement contract supplied
Runtime performancenot-selectedNo target hardware/runtime contract supplied

Pack inventory and content evidence

The external inventory contains 22 FBX files and 20 manifest-declared motions. Archive member counts exceed manifest motion counts by one non-motion/reference asset per archive. Delivered metadata declares 30 fps FBX export preferences, but this is not a target-engine import result.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
No mechanical errors22 filesFile-ready mechanical health onlyobserved-animsmith, AnimSmith 0.10.0
duration-sanity warnings3 findingsTwo no-track clips and one channel-end mismatch require semantic classification or reviewobserved-animsmith, AnimSmith 0.10.0
constant-track notesEntire corpusHygiene evidence, not authorization to rewriteobserved-animsmith

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Variant-level ownership mismatchFBX fix --dry-run representativeSafe refusal: fix accepts only glTF/GLBCaptured exit 2 and stderr under AnimSmith 0.10.0No conversion or transform was authorized

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityunspecifiedNot runnot-evaluatedExact import/controller test
Unreal EngineunspecifiedNot runnot-evaluatedExact import/retarget test
GodotunspecifiedNot runnot-evaluatedExact import/AnimationTree test
BevyunspecifiedNot runnot-evaluatedExact loader/graph test

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Delivered corpus / target projectMixamo profile resolved for lint onlyNot evaluatedArchive variant labels insufficient per clipNot evaluatedunknown

Limitations and unknowns

  1. License, target engine, project controller, runtime-set membership, source-to-manifest member mapping, retargeting, visual/contact quality, and cross-pack compatibility remain unverified.

Changes between AnimSmith versions

AnimSmith 0.10.0 — Official v0.10.0 rerun revalidated output schema v19 and measurements schema v18: 22/22 FBX admitted, baseline 0 errors, and the root-motion declaration found 3 stationary-root errors. AnimSmith 0.7.0 (output schema v17; measurements schema v16) is retained historical evidence only.

Reproduction

working-tree state is N/A (official release artifact).

Reproduction used the official release artifact: URL https://github.com/mmannerm/animsmith/releases/download/v0.10.0/animsmith-v0.10.0-x86_64-unknown-linux-gnu.tar.gz; archive SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith; binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387. Official tag is v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f; working-tree state N/A; compiled features fbx,report; output schema v19 and measurements schema v18.

Required evaluator checks succeeded for --version, top-level --help, inspect --help, measure --help, lint --help, and report --help, and representative FBX admission succeeded. Safe normalized preflight status is preflight-status-v1, SHA-256 739930cd9c04189be3ffe1d3f7381800898d8d4b35c75c9a519e57f6cfad1fad; available help digests are version 35bb24c48d2e2fcf9ae5753338f09722ee50f9c002d00df7d0d30afa5d2ac4a0, top-level 905f42d113783490bc8f3dea6bcda6bfc648a7e2f29b71fec99c41e6701099c8, inspect c45acaabc9e357af7c2c6265a18dccbb67b6cf1cb7dc45263652e0f5fa922c0b, measure cca0b44510c65ee671ac13b591712b6c67f6b1a4f2efdb0aca4ba26293e3d169, lint e216ccd638df639853687025f4eaab35e03c32d306b0dbcd0cb2c5c19ff53def, and report 8298b2c9358bc796362eae5aeae22d86c9a06843c0dec7eb0f06cee82b27ad03. External empty-baseline and variant-contract configs, inventories, command outputs, exit codes, and remaining digests are retained outside the repository.

Sources

  • Delivered metadata and external AnimSmith 0.10.0 evidence; no licensed payloads are published.

Animation pack evaluation: Mixamo Magic Locomotion

Technical verdict: Insufficient technical evidence

Evaluation completeness: partial — file inspection does not establish license, runtime, visual, contact, retarget, or cross-pack behavior.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Evidence appendix

Technical decision

This constituent is one observed archive pair in the locally held collection. AnimSmith 0.10.0 mechanically inspected all 27 extracted FBX files with no errors in the untouched baseline. The baseline retained 6 duration-sanity warnings and 3,837 constant-track notes; these are not gameplay or engine acceptance. The archive-level in-place contract produced no errors, while the archive-level root-motion contract produced 4 stationary-root errors and 5 duration-sanity warnings. No source bytes were changed. The readiness ladder remains the governing boundary.

Capability coverage

Complete core

  • File inventory, parsing, inspection, measurement, and baseline mechanical lint were completed for the delivered files.

Partial supporting gameplay

  • Archive-level in-place and root-motion labels supported only a variant-level XZ contract; per-clip movement ownership is not established.

Absent

  • No license terms, target engine import, blend graph, retargeting, contact, visual, performance, or production acceptance evidence was supplied.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=not-evaluated; do not construct a blend set from archive labels alone.
  2. Timing/synchronization: sync=not-evaluated; no per-member loop or phase contract was established.
  3. State ownership: owner=per-clip-declaration; set XZ, vertical, and yaw ownership only in the target project.
  4. Composition constraints: composition=full-body-default; do not promote masks, additive layers, or contacts without runtime evidence.
  5. Acceptance gate: gate=target-engine-visual-test; import, playback, blend, retarget, and visual/contact testing remain required.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
MIX-OWN-001moderateArchive-level root-motion labeling includes stationary-root files, so a package-wide animation-owned XZ policy can make characters run in place. Guidance: not applicable.unknownDeclare ownership per clip after target-controller review.Not a generic rewrite; it needs semantic/project intent.observed-animsmith; partial

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unity unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and controller
Unreal Engine unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and retarget path
Godot unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and AnimationTree
Bevy unspecifiednot-evaluatedNo import or playback evidenceTest exact loader and graph

Fit and limitations

Best fit is a controlled evaluation intake where a project can supply per-clip intent and test its own controller. It is a poor fit for immediate drop-in compatibility claims. Cross-pack compatibility is unknown and must be tested against the exact intended constituents and target rig.

Changes between AnimSmith versions

AnimSmith 0.10.0 refresh — all baseline and archive-variant contract runs were repeated with tag v0.10.0. The retained 0.7.0 result had the same 6 baseline warnings and 4 stationary-root contract findings; the current output records the full current contract result separately, including 5 duration-sanity warnings.

Evidence status

27 delivered FBX files and 25 retained manifest-declared motions were evaluated mechanically with AnimSmith 0.10.0 at revision db91d8dda3326f97f581d4d62104d928caec383f. The validated external manifests use scrubbed SHA-256 identities only. The companion appendix records the current schemas and open evidence gates.

Sources

  • Delivered archive manifests and AnimSmith 0.10.0 output, retained externally as scrubbed evidence.

Animation pack evidence appendix: Mixamo Magic Locomotion

Companion report: Technical report

Evidence status: partial — source archives were evaluated externally; licensing, runtime, and visual evidence remain unavailable.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

This appendix records scrubbed evidence only. The canonical readiness ladder remains authoritative.

Evaluation scope and provenance

FieldValue
Pack/editionMixamo Magic Locomotion; local revision unknown
Vendor/sourceVendor identity observed in delivered metadata; listing URL not retained
Delivered scope27 extracted FBX files from paired archive variants
Target useEngine-neutral marketplace intake
Target enginesNot evaluated
Target rigs/packsOne declared character identity; cross-pack use not evaluated
Source manifestScrubbed SHA-256 retained externally
Evaluation manifestmixamo-magic-locomotion.manifest.json; SHA-256 c766dea7a1f0e34d83fcf7f4686f9540f72b514643e28038b81e9ea9a1c8909d; schema urn:animsmith:skill:animation-pack-evaluation-manifest:1
Acquisition/license provenanceNo controlling license/readme/terms evidence was delivered; not legal advice

The evaluation manifest schema is urn:animsmith:skill:animation-pack-evaluation-manifest:1. The validated manifest models every delivered FBX as an opaque other-unknown unit because vendor records were not linked to archive members; 25 remains separate source metadata.

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files27276 baseline warningsSemantic file-to-manifest mapping incomplete
Rigs/export variants11Mixamo profile usedRetarget behavior unavailable
AnimSmith baseline27270 errors—
Declared contracts27274 errors; 5 warningsArchive-level ownership only; per-clip intent unavailable
AnimSmith visual reports100One representative current HTML report generated externally but not visually inspected
Engine import/playback400No engines/project supplied
Blend/mask/retarget100No runtime contract supplied

Claim legend

Claims use observed-file, observed-animsmith, and not-evaluated as defined by the assessment taxonomy.

Evaluation manifest and taxonomy

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00Not classified
continuous-locomotion00Not classified
locomotion-transition00Not classified
airborne00Not classified
traversal00Not classified
action-interaction00Not classified
reaction-death00Not classified
emote-cinematic00Not classified
other-unknown2727One opaque unit per delivered FBX; no semantic classification asserted
Total2727Validated scrubbed external manifest

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquirepartially-evaluatedLocal archive identity recorded; license terms unavailable
Preserve rawevaluated-cleanArchives were not modified; extraction was external
Inspectevaluated-cleanAnimSmith 0.10.0 inspected every extracted FBX
Segmentnot-evaluatedManifest-to-member clip linkage was not established
Root motionpartially-evaluatedVariant-level XZ contracts ran; per-clip ownership remains open
Conformnot-evaluatedNo target contract authorized transforms
Validatepartially-evaluatedMechanical baseline and variant contracts ran; engine validation deferred
Optimizenot-evaluatedNo optimization was authorized
Exportnot-evaluatedNo engine-facing export was evaluated
Gate/reportpartially-evaluatedThis scrubbed report retains the evidence boundary

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
Unclassified delivered corpusMechanical baseline completed with no errorsPer-clip intent, loops, and ownership incompleteEngine, retarget, contact, visual, and gameplay acceptance not evaluated

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — observed-pack-capabilityExternal inventory and baseline completed; license evidence remains unavailable
Blended locomotionselected — observed-pack-capabilityRuntime sets and engine blend tests not evaluated
Root-motion controllerselected — observed-pack-capabilityVariant-level ownership differs; choose per-clip policy in the target project
State-machine transitionsnot-selectedNo target state-machine contract supplied
Layered upper body/weaponsnot-selectedNo mask or weapon contract supplied
Traversal/environmentnot-selectedNo target traversal contract supplied
Contact actions/interactionsnot-selectedNo target contact contract supplied
Retargeted/customizable charactersnot-selectedNo target rig or retargeter supplied
Motion matching/searchnot-selectedNo search runtime contract supplied
Networked movementnot-selectedNo networked movement contract supplied
Runtime performancenot-selectedNo target hardware/runtime contract supplied

Pack inventory and content evidence

The external inventory contains 27 FBX files and 25 manifest-declared motions. Archive member counts exceed manifest motion counts by one non-motion/reference asset per archive. Delivered metadata declares 30 fps FBX export preferences, but this is not a target-engine import result.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
No mechanical errors27 filesFile-ready mechanical health onlyobserved-animsmith
duration-sanity warnings6 filesNo-track assets/clips require semantic classificationobserved-animsmith
constant-track notesEntire corpusHygiene evidence, not authorization to rewriteobserved-animsmith

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Variant-level ownership mismatchAnimSmith 0.10.0 FBX fix --dry-run representativeSafe refusal: fix accepts only glTF/GLBCaptured exit 2 and stderrNo conversion or transform was authorized

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityunspecifiedNot runnot-evaluatedExact import/controller test
Unreal EngineunspecifiedNot runnot-evaluatedExact import/retarget test
GodotunspecifiedNot runnot-evaluatedExact import/AnimationTree test
BevyunspecifiedNot runnot-evaluatedExact loader/graph test

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Delivered corpus / target projectMixamo profile resolved for lint onlyNot evaluatedArchive variant labels insufficient per clipNot evaluatedunknown

Limitations and unknowns

  1. License, target engine, project controller, runtime-set membership, source-to-manifest member mapping, retargeting, visual/contact quality, and cross-pack compatibility remain unverified.

Changes between AnimSmith versions

AnimSmith 0.10.0 refresh — all 27 baseline files and both archive-variant contracts were rerun. The retained 0.7.0 baseline had the same 6 warnings and 4 stationary-root findings; the current root-motion contract also records 5 duration-sanity warnings.

Reproduction

working-tree state is N/A (official release artifact).

Reproduction used the official release artifact: URL https://github.com/mmannerm/animsmith/releases/download/v0.10.0/animsmith-v0.10.0-x86_64-unknown-linux-gnu.tar.gz; archive SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith; binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387. Official tag is v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f; working-tree state N/A; compiled features fbx,report; output schema v19 and measurements schema v18.

Required evaluator checks succeeded for --version, top-level --help, inspect --help, measure --help, lint --help, and report --help, and representative FBX admission succeeded. Safe normalized preflight status is preflight-status-v1, SHA-256 739930cd9c04189be3ffe1d3f7381800898d8d4b35c75c9a519e57f6cfad1fad; available help digests are version 35bb24c48d2e2fcf9ae5753338f09722ee50f9c002d00df7d0d30afa5d2ac4a0, top-level 905f42d113783490bc8f3dea6bcda6bfc648a7e2f29b71fec99c41e6701099c8, inspect c45acaabc9e357af7c2c6265a18dccbb67b6cf1cb7dc45263652e0f5fa922c0b, measure cca0b44510c65ee671ac13b591712b6c67f6b1a4f2efdb0aca4ba26293e3d169, lint e216ccd638df639853687025f4eaab35e03c32d306b0dbcd0cb2c5c19ff53def, and report 8298b2c9358bc796362eae5aeae22d86c9a06843c0dec7eb0f06cee82b27ad03. empty.toml SHA-256 cf70223ed7398b5419e96394fa30b597f1bf496217715319e478266207270c05; in-place.toml SHA-256 acb638c0dd6397d98d1f674d8ef077d3d6deaea796514ebd9f5ea749ccc24dc0; root-motion.toml SHA-256 567dd18f479c79724e322cb0c3ef52485601f59caebd3a95eea77bb6ec6ae191.

The external run used inspect --config empty.toml per file, batch measure --format json and lint --format json/markdown against empty.toml, then archive-variant batch lint --format json against each contract; all baseline commands exited 0, the root-motion contract exited 1 for its findings, and FBX fix --dry-run exited 2 because fix accepts only glTF/GLB. Source and extracted inventories, command outputs, exit codes, HTML reports, and SHA-256 digests remain outside the repository.

Before exhaustive execution, the external evidence captured top-level and required-command help plus a representative delivered FBX admission: inspect, measure, lint, and report each exited 0.

Sources

  • Delivered metadata and external AnimSmith evidence; no licensed payloads are published.

Animation pack evaluation: Mixamo Male Locomotion

Technical verdict: Insufficient technical evidence

Evaluation completeness: partial — file inspection does not establish license, runtime, visual, contact, retarget, or cross-pack behavior.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Evidence appendix

Technical decision

This constituent is one observed archive pair in the locally held collection. AnimSmith 0.10.0 mechanically inspected all 18 extracted FBX files with no errors in the untouched baseline. The baseline retained 5 duration-sanity warnings and 2,564 constant-track notes; these are not gameplay or engine acceptance. The archive-level in-place contract produced no errors, while the archive-level root-motion contract produced 4 stationary-root errors and 4 duration-sanity warnings. No source bytes were changed. The readiness ladder remains the governing boundary.

Capability coverage

Complete core

  • File inventory, parsing, inspection, measurement, and baseline mechanical lint were completed for the delivered files.

Partial supporting gameplay

  • Archive-level in-place and root-motion labels supported only a variant-level XZ contract; per-clip movement ownership is not established.

Absent

  • No license terms, target engine import, blend graph, retargeting, contact, visual, performance, or production acceptance evidence was supplied.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=not-evaluated; do not construct a blend set from archive labels alone.
  2. Timing/synchronization: sync=not-evaluated; no per-member loop or phase contract was established.
  3. State ownership: owner=per-clip-declaration; set XZ, vertical, and yaw ownership only in the target project.
  4. Composition constraints: composition=full-body-default; do not promote masks, additive layers, or contacts without runtime evidence.
  5. Acceptance gate: gate=target-engine-visual-test; import, playback, blend, retarget, and visual/contact testing remain required.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
MIX-OWN-001moderateArchive-level root-motion labeling includes stationary-root files, so a package-wide animation-owned XZ policy can make characters run in place. Guidance: not applicable.unknownDeclare ownership per clip after target-controller review.Not a generic rewrite; it needs semantic/project intent.observed-animsmith; partial

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unity unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and controller
Unreal Engine unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and retarget path
Godot unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and AnimationTree
Bevy unspecifiednot-evaluatedNo import or playback evidenceTest exact loader and graph

Fit and limitations

Best fit is a controlled evaluation intake where a project can supply per-clip intent and test its own controller. It is a poor fit for immediate drop-in compatibility claims. Cross-pack compatibility is unknown and must be tested against the exact intended constituents and target rig.

Changes between AnimSmith versions

AnimSmith 0.10.0 refresh — all baseline and archive-variant contract runs were repeated with tag v0.10.0. The retained 0.7.0 result had the same 5 baseline warnings and 4 stationary-root contract findings; the current output records the full current contract result separately, including 4 duration-sanity warnings.

Evidence status

18 delivered FBX files and 16 retained manifest-declared motions were evaluated mechanically with AnimSmith 0.10.0 at revision db91d8dda3326f97f581d4d62104d928caec383f. The validated external manifests use scrubbed SHA-256 identities only. The companion appendix records the current schemas and open evidence gates.

Sources

  • Delivered archive manifests and AnimSmith 0.10.0 output, retained externally as scrubbed evidence.

Animation pack evidence appendix: Mixamo Male Locomotion

Companion report: Technical report

Evidence status: partial — source archives were evaluated externally; licensing, runtime, and visual evidence remain unavailable.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

This appendix records scrubbed evidence only. The canonical readiness ladder remains authoritative.

Evaluation scope and provenance

FieldValue
Pack/editionMixamo Male Locomotion; local revision unknown
Vendor/sourceVendor identity observed in delivered metadata; listing URL not retained
Delivered scope18 extracted FBX files from paired archive variants
Target useEngine-neutral marketplace intake
Target enginesNot evaluated
Target rigs/packsOne declared character identity; cross-pack use not evaluated
Source manifestScrubbed SHA-256 retained externally
Evaluation manifestmixamo-male-locomotion.manifest.json; SHA-256 bebd0e4f450133c8a00b9d2c8bb9cb996a56a8ab43cfc7f2d8b4c07c44a8f288; schema urn:animsmith:skill:animation-pack-evaluation-manifest:1
Acquisition/license provenanceNo controlling license/readme/terms evidence was delivered; not legal advice

The evaluation manifest schema is urn:animsmith:skill:animation-pack-evaluation-manifest:1. The validated manifest models every delivered FBX as an opaque other-unknown unit because vendor records were not linked to archive members; 16 remains separate source metadata.

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files18185 baseline warningsSemantic file-to-manifest mapping incomplete
Rigs/export variants11Mixamo profile usedRetarget behavior unavailable
AnimSmith baseline18180 errors—
Declared contracts18184 errors; 4 warningsArchive-level ownership only; per-clip intent unavailable
AnimSmith visual reports100One representative current HTML report generated externally but not visually inspected
Engine import/playback400No engines/project supplied
Blend/mask/retarget100No runtime contract supplied

Claim legend

Claims use observed-file, observed-animsmith, and not-evaluated as defined by the assessment taxonomy.

Evaluation manifest and taxonomy

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00Not classified
continuous-locomotion00Not classified
locomotion-transition00Not classified
airborne00Not classified
traversal00Not classified
action-interaction00Not classified
reaction-death00Not classified
emote-cinematic00Not classified
other-unknown1818One opaque unit per delivered FBX; no semantic classification asserted
Total1818Validated scrubbed external manifest

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquirepartially-evaluatedLocal archive identity recorded; license terms unavailable
Preserve rawevaluated-cleanArchives were not modified; extraction was external
Inspectevaluated-cleanAnimSmith 0.10.0 inspected every extracted FBX
Segmentnot-evaluatedManifest-to-member clip linkage was not established
Root motionpartially-evaluatedVariant-level XZ contracts ran; per-clip ownership remains open
Conformnot-evaluatedNo target contract authorized transforms
Validatepartially-evaluatedMechanical baseline and variant contracts ran; engine validation deferred
Optimizenot-evaluatedNo optimization was authorized
Exportnot-evaluatedNo engine-facing export was evaluated
Gate/reportpartially-evaluatedThis scrubbed report retains the evidence boundary

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
Unclassified delivered corpusMechanical baseline completed with no errorsPer-clip intent, loops, and ownership incompleteEngine, retarget, contact, visual, and gameplay acceptance not evaluated

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — observed-pack-capabilityExternal inventory and baseline completed; license evidence remains unavailable
Blended locomotionselected — observed-pack-capabilityRuntime sets and engine blend tests not evaluated
Root-motion controllerselected — observed-pack-capabilityVariant-level ownership differs; choose per-clip policy in the target project
State-machine transitionsnot-selectedNo target state-machine contract supplied
Layered upper body/weaponsnot-selectedNo mask or weapon contract supplied
Traversal/environmentnot-selectedNo target traversal contract supplied
Contact actions/interactionsnot-selectedNo target contact contract supplied
Retargeted/customizable charactersnot-selectedNo target rig or retargeter supplied
Motion matching/searchnot-selectedNo search runtime contract supplied
Networked movementnot-selectedNo networked movement contract supplied
Runtime performancenot-selectedNo target hardware/runtime contract supplied

Pack inventory and content evidence

The external inventory contains 18 FBX files and 16 manifest-declared motions. Archive member counts exceed manifest motion counts by one non-motion/reference asset per archive. Delivered metadata declares 30 fps FBX export preferences, but this is not a target-engine import result.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
No mechanical errors18 filesFile-ready mechanical health onlyobserved-animsmith
duration-sanity warnings5 filesNo-track assets/clips require semantic classificationobserved-animsmith
constant-track notesEntire corpusHygiene evidence, not authorization to rewriteobserved-animsmith

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Variant-level ownership mismatchAnimSmith 0.10.0 FBX fix --dry-run representativeSafe refusal: fix accepts only glTF/GLBCaptured exit 2 and stderrNo conversion or transform was authorized

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityunspecifiedNot runnot-evaluatedExact import/controller test
Unreal EngineunspecifiedNot runnot-evaluatedExact import/retarget test
GodotunspecifiedNot runnot-evaluatedExact import/AnimationTree test
BevyunspecifiedNot runnot-evaluatedExact loader/graph test

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Delivered corpus / target projectMixamo profile resolved for lint onlyNot evaluatedArchive variant labels insufficient per clipNot evaluatedunknown

Limitations and unknowns

  1. License, target engine, project controller, runtime-set membership, source-to-manifest member mapping, retargeting, visual/contact quality, and cross-pack compatibility remain unverified.

Changes between AnimSmith versions

AnimSmith 0.10.0 refresh — all 18 baseline files and both archive-variant contracts were rerun. The retained 0.7.0 baseline had the same 5 warnings and 4 stationary-root findings; the current root-motion contract also records 4 duration-sanity warnings.

Reproduction

working-tree state is N/A (official release artifact).

Reproduction used the official release artifact: URL https://github.com/mmannerm/animsmith/releases/download/v0.10.0/animsmith-v0.10.0-x86_64-unknown-linux-gnu.tar.gz; archive SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith; binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387. Official tag is v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f; working-tree state N/A; compiled features fbx,report; output schema v19 and measurements schema v18.

Required evaluator checks succeeded for --version, top-level --help, inspect --help, measure --help, lint --help, and report --help, and representative FBX admission succeeded. Safe normalized preflight status is preflight-status-v1, SHA-256 739930cd9c04189be3ffe1d3f7381800898d8d4b35c75c9a519e57f6cfad1fad; available help digests are version 35bb24c48d2e2fcf9ae5753338f09722ee50f9c002d00df7d0d30afa5d2ac4a0, top-level 905f42d113783490bc8f3dea6bcda6bfc648a7e2f29b71fec99c41e6701099c8, inspect c45acaabc9e357af7c2c6265a18dccbb67b6cf1cb7dc45263652e0f5fa922c0b, measure cca0b44510c65ee671ac13b591712b6c67f6b1a4f2efdb0aca4ba26293e3d169, lint e216ccd638df639853687025f4eaab35e03c32d306b0dbcd0cb2c5c19ff53def, and report 8298b2c9358bc796362eae5aeae22d86c9a06843c0dec7eb0f06cee82b27ad03. empty.toml SHA-256 cf70223ed7398b5419e96394fa30b597f1bf496217715319e478266207270c05; in-place.toml SHA-256 acb638c0dd6397d98d1f674d8ef077d3d6deaea796514ebd9f5ea749ccc24dc0; root-motion.toml SHA-256 567dd18f479c79724e322cb0c3ef52485601f59caebd3a95eea77bb6ec6ae191.

The external run used inspect --config empty.toml per file, batch measure --format json and lint --format json/markdown against empty.toml, then archive-variant batch lint --format json against each contract; all baseline commands exited 0, the root-motion contract exited 1 for its findings, and FBX fix --dry-run exited 2 because fix accepts only glTF/GLB. Source and extracted inventories, command outputs, exit codes, HTML reports, and SHA-256 digests remain outside the repository.

Before exhaustive execution, the external evidence captured top-level and required-command help plus a representative delivered FBX admission: inspect, measure, lint, and report each exited 0.

Sources

  • Delivered metadata and external AnimSmith evidence; no licensed payloads are published.

Animation pack evaluation: Mixamo Pistol-Handgun Locomotion

Technical verdict: Insufficient technical evidence

Evaluation completeness: partial — file inspection does not establish license, runtime, visual, contact, retarget, or cross-pack behavior.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Evidence appendix

Technical decision

This constituent is one observed archive pair in the locally held collection. AnimSmith 0.10.0 mechanically inspected all 29 extracted FBX files with no errors in the untouched baseline. The baseline retained 7 duration-sanity warnings and 4,122 constant-track notes; these are not gameplay or engine acceptance. The archive-level in-place contract produced no errors, while the archive-level root-motion contract produced 5 stationary-root errors and 6 duration-sanity warnings. No source bytes were changed. The readiness ladder remains the governing boundary.

Capability coverage

Complete core

  • File inventory, parsing, inspection, measurement, and baseline mechanical lint were completed for the delivered files.

Partial supporting gameplay

  • Archive-level in-place and root-motion labels supported only a variant-level XZ contract; per-clip movement ownership is not established.

Absent

  • No license terms, target engine import, blend graph, retargeting, contact, visual, performance, or production acceptance evidence was supplied.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=not-evaluated; do not construct a blend set from archive labels alone.
  2. Timing/synchronization: sync=not-evaluated; no per-member loop or phase contract was established.
  3. State ownership: owner=per-clip-declaration; set XZ, vertical, and yaw ownership only in the target project.
  4. Composition constraints: composition=full-body-default; do not promote masks, additive layers, or contacts without runtime evidence.
  5. Acceptance gate: gate=target-engine-visual-test; import, playback, blend, retarget, and visual/contact testing remain required.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
MIX-OWN-001moderateArchive-level root-motion labeling includes stationary-root files, so a package-wide animation-owned XZ policy can make characters run in place. Guidance: not applicable.unknownDeclare ownership per clip after target-controller review.Not a generic rewrite; it needs semantic/project intent.observed-animsmith; partial

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unity unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and controller
Unreal Engine unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and retarget path
Godot unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and AnimationTree
Bevy unspecifiednot-evaluatedNo import or playback evidenceTest exact loader and graph

Fit and limitations

Best fit is a controlled evaluation intake where a project can supply per-clip intent and test its own controller. It is a poor fit for immediate drop-in compatibility claims. Cross-pack compatibility is unknown and must be tested against the exact intended constituents and target rig.

Changes between AnimSmith versions

AnimSmith 0.10.0 refresh — all baseline and archive-variant contract runs were repeated with tag v0.10.0. The retained 0.7.0 result had the same 7 baseline warnings and 5 stationary-root contract findings; the current output records the full current contract result separately, including 6 duration-sanity warnings.

Evidence status

29 delivered FBX files and 27 retained manifest-declared motions were evaluated mechanically with AnimSmith 0.10.0 at revision db91d8dda3326f97f581d4d62104d928caec383f. The validated external manifests use scrubbed SHA-256 identities only. The companion appendix records the current schemas and open evidence gates.

Sources

  • Delivered archive manifests and AnimSmith 0.10.0 output, retained externally as scrubbed evidence.

Animation pack evidence appendix: Mixamo Pistol-Handgun Locomotion

Companion report: Technical report

Evidence status: partial — source archives were evaluated externally; licensing, runtime, and visual evidence remain unavailable.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

This appendix records scrubbed evidence only. The canonical readiness ladder remains authoritative.

Evaluation scope and provenance

FieldValue
Pack/editionMixamo Pistol-Handgun Locomotion; local revision unknown
Vendor/sourceVendor identity observed in delivered metadata; listing URL not retained
Delivered scope29 extracted FBX files from paired archive variants
Target useEngine-neutral marketplace intake
Target enginesNot evaluated
Target rigs/packsOne declared character identity; cross-pack use not evaluated
Source manifestScrubbed SHA-256 retained externally
Evaluation manifestmixamo-pistol-handgun-locomotion.manifest.json; SHA-256 5fbc91e885d4dbdacf25a67c26833a0c9f6c48b410555b67c5cd0843e7bbd47b; schema urn:animsmith:skill:animation-pack-evaluation-manifest:1
Acquisition/license provenanceNo controlling license/readme/terms evidence was delivered; not legal advice

The evaluation manifest schema is urn:animsmith:skill:animation-pack-evaluation-manifest:1. The validated manifest models every delivered FBX as an opaque other-unknown unit because vendor records were not linked to archive members; 27 remains separate source metadata.

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files29297 baseline warningsSemantic file-to-manifest mapping incomplete
Rigs/export variants11Mixamo profile usedRetarget behavior unavailable
AnimSmith baseline29290 errors—
Declared contracts29295 errors; 6 warningsArchive-level ownership only; per-clip intent unavailable
AnimSmith visual reports100One representative current HTML report generated externally but not visually inspected
Engine import/playback400No engines/project supplied
Blend/mask/retarget100No runtime contract supplied

Claim legend

Claims use observed-file, observed-animsmith, and not-evaluated as defined by the assessment taxonomy.

Evaluation manifest and taxonomy

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00Not classified
continuous-locomotion00Not classified
locomotion-transition00Not classified
airborne00Not classified
traversal00Not classified
action-interaction00Not classified
reaction-death00Not classified
emote-cinematic00Not classified
other-unknown2929One opaque unit per delivered FBX; no semantic classification asserted
Total2929Validated scrubbed external manifest

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquirepartially-evaluatedLocal archive identity recorded; license terms unavailable
Preserve rawevaluated-cleanArchives were not modified; extraction was external
Inspectevaluated-cleanAnimSmith 0.10.0 inspected every extracted FBX
Segmentnot-evaluatedManifest-to-member clip linkage was not established
Root motionpartially-evaluatedVariant-level XZ contracts ran; per-clip ownership remains open
Conformnot-evaluatedNo target contract authorized transforms
Validatepartially-evaluatedMechanical baseline and variant contracts ran; engine validation deferred
Optimizenot-evaluatedNo optimization was authorized
Exportnot-evaluatedNo engine-facing export was evaluated
Gate/reportpartially-evaluatedThis scrubbed report retains the evidence boundary

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
Unclassified delivered corpusMechanical baseline completed with no errorsPer-clip intent, loops, and ownership incompleteEngine, retarget, contact, visual, and gameplay acceptance not evaluated

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — observed-pack-capabilityExternal inventory and baseline completed; license evidence remains unavailable
Blended locomotionselected — observed-pack-capabilityRuntime sets and engine blend tests not evaluated
Root-motion controllerselected — observed-pack-capabilityVariant-level ownership differs; choose per-clip policy in the target project
State-machine transitionsnot-selectedNo target state-machine contract supplied
Layered upper body/weaponsnot-selectedNo mask or weapon contract supplied
Traversal/environmentnot-selectedNo target traversal contract supplied
Contact actions/interactionsnot-selectedNo target contact contract supplied
Retargeted/customizable charactersnot-selectedNo target rig or retargeter supplied
Motion matching/searchnot-selectedNo search runtime contract supplied
Networked movementnot-selectedNo networked movement contract supplied
Runtime performancenot-selectedNo target hardware/runtime contract supplied

Pack inventory and content evidence

The external inventory contains 29 FBX files and 27 manifest-declared motions. Archive member counts exceed manifest motion counts by one non-motion/reference asset per archive. Delivered metadata declares 30 fps FBX export preferences, but this is not a target-engine import result.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
No mechanical errors29 filesFile-ready mechanical health onlyobserved-animsmith
duration-sanity warnings7 filesNo-track assets/clips require semantic classificationobserved-animsmith
constant-track notesEntire corpusHygiene evidence, not authorization to rewriteobserved-animsmith

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Variant-level ownership mismatchAnimSmith 0.10.0 FBX fix --dry-run representativeSafe refusal: fix accepts only glTF/GLBCaptured exit 2 and stderrNo conversion or transform was authorized

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityunspecifiedNot runnot-evaluatedExact import/controller test
Unreal EngineunspecifiedNot runnot-evaluatedExact import/retarget test
GodotunspecifiedNot runnot-evaluatedExact import/AnimationTree test
BevyunspecifiedNot runnot-evaluatedExact loader/graph test

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Delivered corpus / target projectMixamo profile resolved for lint onlyNot evaluatedArchive variant labels insufficient per clipNot evaluatedunknown

Limitations and unknowns

  1. License, target engine, project controller, runtime-set membership, source-to-manifest member mapping, retargeting, visual/contact quality, and cross-pack compatibility remain unverified.

Changes between AnimSmith versions

AnimSmith 0.10.0 refresh — all 29 baseline files and both archive-variant contracts were rerun. The retained 0.7.0 baseline had the same 7 warnings and 5 stationary-root findings; the current root-motion contract also records 6 duration-sanity warnings.

Reproduction

working-tree state is N/A (official release artifact).

Reproduction used the official release artifact: URL https://github.com/mmannerm/animsmith/releases/download/v0.10.0/animsmith-v0.10.0-x86_64-unknown-linux-gnu.tar.gz; archive SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith; binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387. Official tag is v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f; working-tree state N/A; compiled features fbx,report; output schema v19 and measurements schema v18.

Required evaluator checks succeeded for --version, top-level --help, inspect --help, measure --help, lint --help, and report --help, and representative FBX admission succeeded. Safe normalized preflight status is preflight-status-v1, SHA-256 739930cd9c04189be3ffe1d3f7381800898d8d4b35c75c9a519e57f6cfad1fad; available help digests are version 35bb24c48d2e2fcf9ae5753338f09722ee50f9c002d00df7d0d30afa5d2ac4a0, top-level 905f42d113783490bc8f3dea6bcda6bfc648a7e2f29b71fec99c41e6701099c8, inspect c45acaabc9e357af7c2c6265a18dccbb67b6cf1cb7dc45263652e0f5fa922c0b, measure cca0b44510c65ee671ac13b591712b6c67f6b1a4f2efdb0aca4ba26293e3d169, lint e216ccd638df639853687025f4eaab35e03c32d306b0dbcd0cb2c5c19ff53def, and report 8298b2c9358bc796362eae5aeae22d86c9a06843c0dec7eb0f06cee82b27ad03. empty.toml SHA-256 cf70223ed7398b5419e96394fa30b597f1bf496217715319e478266207270c05; in-place.toml SHA-256 acb638c0dd6397d98d1f674d8ef077d3d6deaea796514ebd9f5ea749ccc24dc0; root-motion.toml SHA-256 567dd18f479c79724e322cb0c3ef52485601f59caebd3a95eea77bb6ec6ae191.

The external run used inspect --config empty.toml per file, batch measure --format json and lint --format json/markdown against empty.toml, then archive-variant batch lint --format json against each contract; all baseline commands exited 0, the root-motion contract exited 1 for its findings, and FBX fix --dry-run exited 2 because fix accepts only glTF/GLB. Source and extracted inventories, command outputs, exit codes, HTML reports, and SHA-256 digests remain outside the repository.

Before exhaustive execution, the external evidence captured top-level and required-command help plus a representative delivered FBX admission: inspect, measure, lint, and report each exited 0.

Sources

  • Delivered metadata and external AnimSmith evidence; no licensed payloads are published.

Animation pack evaluation: Mixamo Rifle 8-Way Locomotion

Technical verdict: Insufficient technical evidence

Evaluation completeness: partial — file inspection does not establish license, runtime, visual, contact, retarget, or cross-pack behavior.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Evidence appendix

Technical decision

This constituent is one observed archive pair in the locally held collection. AnimSmith 0.10.0 mechanically inspected all 83 extracted FBX files with no errors in the untouched baseline. The baseline retained 11 duration-sanity warnings and 11,790 constant-track notes; these are not gameplay or engine acceptance. The archive-level in-place contract produced no errors, while the archive-level root-motion contract produced 16 stationary-root errors and 8 duration-sanity warnings. No source bytes were changed. The readiness ladder remains the governing boundary.

Capability coverage

Complete core

  • File inventory, parsing, inspection, measurement, and baseline mechanical lint were completed for the delivered files.

Partial supporting gameplay

  • Archive-level in-place and root-motion labels supported only a variant-level XZ contract; per-clip movement ownership is not established.

Absent

  • No license terms, target engine import, blend graph, retargeting, contact, visual, performance, or production acceptance evidence was supplied.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=not-evaluated; do not construct a blend set from archive labels alone.
  2. Timing/synchronization: sync=not-evaluated; no per-member loop or phase contract was established.
  3. State ownership: owner=per-clip-declaration; set XZ, vertical, and yaw ownership only in the target project.
  4. Composition constraints: composition=full-body-default; do not promote masks, additive layers, or contacts without runtime evidence.
  5. Acceptance gate: gate=target-engine-visual-test; import, playback, blend, retarget, and visual/contact testing remain required.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
MIX-OWN-001moderateArchive-level root-motion labeling includes stationary-root files, so a package-wide animation-owned XZ policy can make characters run in place. Guidance: not applicable.unknownDeclare ownership per clip after target-controller review.Not a generic rewrite; it needs semantic/project intent.observed-animsmith; partial

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unity unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and controller
Unreal Engine unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and retarget path
Godot unspecifiednot-evaluatedNo import or playback evidenceTest exact importer and AnimationTree
Bevy unspecifiednot-evaluatedNo import or playback evidenceTest exact loader and graph

Fit and limitations

Best fit is a controlled evaluation intake where a project can supply per-clip intent and test its own controller. It is a poor fit for immediate drop-in compatibility claims. Cross-pack compatibility is unknown and must be tested against the exact intended constituents and target rig.

Changes between AnimSmith versions

AnimSmith 0.10.0 refresh — all baseline and archive-variant contract runs were repeated with tag v0.10.0. The retained 0.7.0 result had the same 11 baseline warnings and 16 stationary-root contract findings; the current output records the full current contract result separately, including 8 duration-sanity warnings.

Evidence status

83 delivered FBX files and 81 retained manifest-declared motions were evaluated mechanically with AnimSmith 0.10.0 at revision db91d8dda3326f97f581d4d62104d928caec383f. The validated external manifests use scrubbed SHA-256 identities only. The companion appendix records the current schemas and open evidence gates.

Sources

  • Delivered archive manifests and AnimSmith 0.10.0 output, retained externally as scrubbed evidence.

Animation pack evidence appendix: Mixamo Rifle 8-Way Locomotion

Companion report: Technical report

Evidence status: partial — source archives were evaluated externally; licensing, runtime, and visual evidence remain unavailable.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

This appendix records scrubbed evidence only. The canonical readiness ladder remains authoritative.

Evaluation scope and provenance

FieldValue
Pack/editionMixamo Rifle 8-Way Locomotion; local revision unknown
Vendor/sourceVendor identity observed in delivered metadata; listing URL not retained
Delivered scope83 extracted FBX files from paired archive variants
Target useEngine-neutral marketplace intake
Target enginesNot evaluated
Target rigs/packsOne declared character identity; cross-pack use not evaluated
Source manifestScrubbed SHA-256 retained externally
Evaluation manifestmixamo-rifle-8-way-locomotion.manifest.json; SHA-256 5f3a266b00f223047ec6ce5e0bef1e4e93c7d55dafd5bb4fc7ba26fee8e85dc4; schema urn:animsmith:skill:animation-pack-evaluation-manifest:1
Acquisition/license provenanceNo controlling license/readme/terms evidence was delivered; not legal advice

The evaluation manifest schema is urn:animsmith:skill:animation-pack-evaluation-manifest:1. The validated manifest models every delivered FBX as an opaque other-unknown unit because vendor records were not linked to archive members; 81 remains separate source metadata.

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files838311 baseline warningsSemantic file-to-manifest mapping incomplete
Rigs/export variants11Mixamo profile usedRetarget behavior unavailable
AnimSmith baseline83830 errors—
Declared contracts838316 errors; 8 warningsArchive-level ownership only; per-clip intent unavailable
AnimSmith visual reports100One representative current HTML report generated externally but not visually inspected
Engine import/playback400No engines/project supplied
Blend/mask/retarget100No runtime contract supplied

Claim legend

Claims use observed-file, observed-animsmith, and not-evaluated as defined by the assessment taxonomy.

Evaluation manifest and taxonomy

Canonical clip-role inventory

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00Not classified
continuous-locomotion00Not classified
locomotion-transition00Not classified
airborne00Not classified
traversal00Not classified
action-interaction00Not classified
reaction-death00Not classified
emote-cinematic00Not classified
other-unknown8383One opaque unit per delivered FBX; no semantic classification asserted
Total8383Validated scrubbed external manifest

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquirepartially-evaluatedLocal archive identity recorded; license terms unavailable
Preserve rawevaluated-cleanArchives were not modified; extraction was external
Inspectevaluated-cleanAnimSmith 0.10.0 inspected every extracted FBX
Segmentnot-evaluatedManifest-to-member clip linkage was not established
Root motionpartially-evaluatedVariant-level XZ contracts ran; per-clip ownership remains open
Conformnot-evaluatedNo target contract authorized transforms
Validatepartially-evaluatedMechanical baseline and variant contracts ran; engine validation deferred
Optimizenot-evaluatedNo optimization was authorized
Exportnot-evaluatedNo engine-facing export was evaluated
Gate/reportpartially-evaluatedThis scrubbed report retains the evidence boundary

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
Unclassified delivered corpusMechanical baseline completed with no errorsPer-clip intent, loops, and ownership incompleteEngine, retarget, contact, visual, and gameplay acceptance not evaluated

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — observed-pack-capabilityExternal inventory and baseline completed; license evidence remains unavailable
Blended locomotionselected — observed-pack-capabilityRuntime sets and engine blend tests not evaluated
Root-motion controllerselected — observed-pack-capabilityVariant-level ownership differs; choose per-clip policy in the target project
State-machine transitionsnot-selectedNo target state-machine contract supplied
Layered upper body/weaponsnot-selectedNo mask or weapon contract supplied
Traversal/environmentnot-selectedNo target traversal contract supplied
Contact actions/interactionsnot-selectedNo target contact contract supplied
Retargeted/customizable charactersnot-selectedNo target rig or retargeter supplied
Motion matching/searchnot-selectedNo search runtime contract supplied
Networked movementnot-selectedNo networked movement contract supplied
Runtime performancenot-selectedNo target hardware/runtime contract supplied

Pack inventory and content evidence

The external inventory contains 83 FBX files and 81 manifest-declared motions. Archive member counts exceed manifest motion counts by one non-motion/reference asset per archive. Delivered metadata declares 30 fps FBX export preferences, but this is not a target-engine import result.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
No mechanical errors83 filesFile-ready mechanical health onlyobserved-animsmith
duration-sanity warnings11 filesNo-track assets/clips require semantic classificationobserved-animsmith
constant-track notesEntire corpusHygiene evidence, not authorization to rewriteobserved-animsmith

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Variant-level ownership mismatchAnimSmith 0.10.0 FBX fix --dry-run representativeSafe refusal: fix accepts only glTF/GLBCaptured exit 2 and stderrNo conversion or transform was authorized

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityunspecifiedNot runnot-evaluatedExact import/controller test
Unreal EngineunspecifiedNot runnot-evaluatedExact import/retarget test
GodotunspecifiedNot runnot-evaluatedExact import/AnimationTree test
BevyunspecifiedNot runnot-evaluatedExact loader/graph test

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Delivered corpus / target projectMixamo profile resolved for lint onlyNot evaluatedArchive variant labels insufficient per clipNot evaluatedunknown

Limitations and unknowns

  1. License, target engine, project controller, runtime-set membership, source-to-manifest member mapping, retargeting, visual/contact quality, and cross-pack compatibility remain unverified.

Changes between AnimSmith versions

AnimSmith 0.10.0 refresh — all 83 baseline files and both archive-variant contracts were rerun. The retained 0.7.0 baseline had the same 11 warnings and 16 stationary-root findings; the current root-motion contract also records 8 duration-sanity warnings.

Reproduction

working-tree state is N/A (official release artifact).

Reproduction used the official release artifact: URL https://github.com/mmannerm/animsmith/releases/download/v0.10.0/animsmith-v0.10.0-x86_64-unknown-linux-gnu.tar.gz; archive SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith; binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387. Official tag is v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f; working-tree state N/A; compiled features fbx,report; output schema v19 and measurements schema v18.

Required evaluator checks succeeded for --version, top-level --help, inspect --help, measure --help, lint --help, and report --help, and representative FBX admission succeeded. Safe normalized preflight status is preflight-status-v1, SHA-256 739930cd9c04189be3ffe1d3f7381800898d8d4b35c75c9a519e57f6cfad1fad; available help digests are version 35bb24c48d2e2fcf9ae5753338f09722ee50f9c002d00df7d0d30afa5d2ac4a0, top-level 905f42d113783490bc8f3dea6bcda6bfc648a7e2f29b71fec99c41e6701099c8, inspect c45acaabc9e357af7c2c6265a18dccbb67b6cf1cb7dc45263652e0f5fa922c0b, measure cca0b44510c65ee671ac13b591712b6c67f6b1a4f2efdb0aca4ba26293e3d169, lint e216ccd638df639853687025f4eaab35e03c32d306b0dbcd0cb2c5c19ff53def, and report 8298b2c9358bc796362eae5aeae22d86c9a06843c0dec7eb0f06cee82b27ad03. empty.toml SHA-256 cf70223ed7398b5419e96394fa30b597f1bf496217715319e478266207270c05; in-place.toml SHA-256 acb638c0dd6397d98d1f674d8ef077d3d6deaea796514ebd9f5ea749ccc24dc0; root-motion.toml SHA-256 567dd18f479c79724e322cb0c3ef52485601f59caebd3a95eea77bb6ec6ae191.

The external run used inspect --config empty.toml per file, batch measure --format json and lint --format json/markdown against empty.toml, then archive-variant batch lint --format json against each contract; all baseline commands exited 0, the root-motion contract exited 1 for its findings, and FBX fix --dry-run exited 2 because fix accepts only glTF/GLB. Source and extracted inventories, command outputs, exit codes, HTML reports, and SHA-256 digests remain outside the repository.

Before exhaustive execution, the external evidence captured top-level and required-command help plus a representative delivered FBX admission: inspect, measure, lint, and report each exited 0.

Sources

  • Delivered metadata and external AnimSmith evidence; no licensed payloads are published.

Animation pack evaluation: Mixamo Locomotion Collection

Technical verdict: Insufficient technical evidence

Evaluation completeness: partial — this is a documentation rollup of nine current constituent evaluations, not a new collection-level runtime, visual, or compatibility evaluation.

Confidence: medium

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

Detailed evidence: Evidence appendix

Technical decision

This partial rollup aggregates the current evidence for Basic, Female Basic, Female, Locomotion, Longbow, Magic, Male, Pistol/Handgun, and Rifle 8-Way. It makes no new collection-level classification, runtime-set, engine, visual, remediation, retarget, or compatibility claim.

The nine fresh baselines admitted and mechanically checked all 249 extracted FBX files: 0 errors, 50 duration-sanity warnings, and 35,405 non-gating constant-track notes. The declared in-place XZ controls had 0 errors and 13 warnings. The declared root-motion XZ controls had 52 stationary-root errors and 37 warnings. These archive-level declarations are not per-clip movement ownership, and their finding counts must not be pooled with the empty-baseline result. No source bytes were changed. The readiness ladder remains the governing boundary.

Capability coverage

Complete core

  • Current constituent inventory, parsing, inspection, measurement, and empty-baseline mechanical lint cover all 249 delivered FBX files.

Partial supporting gameplay

  • Archive-level in-place and root-motion declarations were exercised, but their labels do not establish per-clip XZ, vertical, or yaw ownership.

Absent

  • No collection-level source-to-manifest member mapping, gameplay classification, runtime-set definition, engine import/playback, blend, retarget, contact, visual, performance, production, or cross-pack compatibility evidence was freshly evaluated.

Runtime sets and authored motion

No important runtime sets were identified.

Integration recipe

  1. Members/topology: topology=not-evaluated; do not construct a collection blend set from archive labels or this rollup.
  2. Timing/synchronization: sync=not-evaluated; no per-member loop, timing, or phase contract was refreshed at collection scope.
  3. State ownership: owner=per-clip-declaration; assign XZ, vertical, and yaw ownership only after target-project review.
  4. Composition constraints: composition=full-body-default; do not promote masks, additive layers, contacts, or retargeting from constituent mechanical evidence.
  5. Acceptance gate: gate=target-engine-visual-test; exact-engine import, playback, blend, retarget, and visual/contact testing remain required.

Technical issue register

IDSeverityProblem and impactPrimary ownerCurrent actionFuture AnimSmith potentialEvidence/status
MIX-OWN-001moderateA root-motion declaration finds 52 stationary-root files, so package-wide animation-owned XZ can make a character run in place. Guidance: not applicable.unknownDeclare movement ownership per clip after target-controller review.Not a generic rewrite; it needs semantic/project intent.observed-animsmith; partial

Engine status

RuntimeEvidence levelTechnical resultRemaining gate
Unity unspecifiednot-evaluatedNo fresh collection import or playback evidenceTest exact importer and controller
Unreal Engine unspecifiednot-evaluatedNo fresh collection import or playback evidenceTest exact importer and retarget path
Godot unspecifiednot-evaluatedNo fresh collection import or playback evidenceTest exact importer and AnimationTree
Bevy unspecifiednot-evaluatedNo fresh collection import or playback evidenceTest exact loader and graph

Fit and limitations

Best fit is a controlled intake where the consuming project supplies per-clip intent and validates its controller. It is a poor fit for immediate drop-in collection compatibility claims.

Cross-pack compatibility remains unknown: this refresh did not compare overlapping paths or skeleton/reference-rig identity, and did not test blend, retarget, mask, contact, or visual behavior.

Changes between AnimSmith versions

AnimSmith 0.10.0 — Fresh constituent inventory, baseline, archive-variant declaration, and preflight evidence revalidated and superseded the prior collection summary. The official evaluator now records output schema v19 and measurements schema v18.

AnimSmith 0.7.0 — Retained only as historical collection evidence; its output schema v17, measurements schema v16, and prior evidence digests are superseded by the current constituent evidence.

Evidence status

The included nine constituents total 249 delivered FBX files and 231 manifest-declared motions. Current evidence used AnimSmith 0.10.0 at peeled source commit db91d8dda3326f97f581d4d62104d928caec383f; source archives, inventories, command outputs, configs, and scrubbed SHA-256 identities remain external. The companion appendix records the exact evaluator provenance and the evidence boundary.

Sources

  • The nine linked constituent reports and their current external AnimSmith 0.10.0 evidence; no licensed payloads are published.

Animation pack evidence appendix: Mixamo Locomotion Collection

Companion report: Technical report

Evidence status: partial — this aggregation retains current constituent evidence only; licensed source archives, runtime tests, visual inspection, and collection-level compatibility tests remain external or unevaluated.

Evaluation date: 2026-09-01

Current evaluator: AnimSmith 0.10.0

Report format: 2

This appendix records scrubbed current evidence only. The canonical readiness ladder remains authoritative.

Evaluation scope and provenance

FieldValue
Pack/editionMixamo Locomotion Collection; local constituent revisions unknown
Vendor/sourceVendor identity observed in constituent delivered metadata; listing URLs not retained
Delivered scope249 extracted FBX files from nine constituent archive pairs; 231 manifest-declared motions retained as separate source metadata
Target useEngine-neutral marketplace-intake aggregation
Target enginesNot evaluated
Target rigs/packsNine named constituent reports; target rig and cross-pack use not evaluated
Source manifestConstituent source and extracted-inventory SHA-256 identities are scrubbed and retained externally
Evaluation manifestNine validated constituent manifests, each urn:animsmith:skill:animation-pack-evaluation-manifest:1; no new collection manifest or classification was generated in this rollup
Acquisition/license provenanceNo controlling license/readme/terms evidence was delivered; not legal advice

Included current reports: Basic, Female Basic, Female, Locomotion, Longbow, Magic, Male, Pistol/Handgun, and Rifle 8-Way. None are excluded from this nine-constituent rollup.

The evaluation manifest schema is urn:animsmith:skill:animation-pack-evaluation-manifest:1. The aggregate uses constituent-manifest file totals and separate manifest-declared-motion metadata. It does not infer a file-to-motion mapping, logical-motion classification, runtime set, or collection compatibility result.

Evidence coverage

SurfaceOffered/deliveredEvaluatedFindingsNot evaluated and why
Animation files2492490 baseline errors; 50 baseline warnings; 35,405 baseline notesConstituent file-to-manifest mapping remains incomplete
Rigs/export variants9 constituent contexts9 lint-profile contextsMixamo profile used for constituent lintRetarget behavior unavailable
AnimSmith baseline2492490 errors; 50 warnings; 35,405 notes—
Declared contracts249249In-place: 0 errors; 13 warnings. Root-motion: 52 stationary-root errors; 37 warningsArchive-level declarations only; per-clip intent unavailable
AnimSmith visual reports1400Generated externally but not visually inspected
Engine import/playback4 runtimes00No engines/project supplied
Blend/mask/retarget1 collection scope00No collection runtime contract supplied

Claim legend

Claims use observed-file, observed-animsmith, and not-evaluated as defined by the assessment taxonomy.

Evaluation manifest and taxonomy

Canonical clip-role inventory

No collection-level canonical role inventory was refreshed. Each constituent's validated manifest preserves its own opaque other-unknown file units; the 231 manifest-declared motions remain separate metadata because source records were not linked to archive members.

Canonical primary roleLogical motionsDelivered filesEvidence boundary
idle-pose00No fresh collection classification
continuous-locomotion00No fresh collection classification
locomotion-transition00No fresh collection classification
airborne00No fresh collection classification
traversal00No fresh collection classification
action-interaction00No fresh collection classification
reaction-death00No fresh collection classification
emote-cinematic00No fresh collection classification
other-unknown249249One opaque unit per delivered FBX in the constituent manifests; no semantic collection classification asserted
Total249249Derived from nine validated constituent manifests; 231 manifest-declared motions are separate metadata

Runtime-set inventory

No runtime sets were identified.

Pipeline-stage coverage

StageCoverage stateEvidence / remaining gate
Acquirepartially-evaluatedConstituent archive identities recorded externally; license terms unavailable
Preserve rawevaluated-cleanConstituent archives were not modified; extraction was external
Inspectevaluated-cleanAnimSmith 0.10.0 inspected every extracted FBX in each constituent baseline
Segmentnot-evaluatedConstituent manifest-to-member clip linkage was not established
Root motionpartially-evaluatedArchive-variant declarations ran; per-clip ownership remains open
Conformnot-evaluatedNo target contract authorized transforms
Validatepartially-evaluatedConstituent mechanical baseline and declarations ran; engine validation deferred
Optimizenot-evaluatedNo optimization was authorized
Exportnot-evaluatedNo engine-facing export was evaluated
Gate/reportpartially-evaluatedThis report aggregates scrubbed current constituent evidence without new collection testing

Readiness evidence by clip set

Role or runtime setFile-ready / clip-readySet-ready / rig-useRuntime / acceptance boundary
Aggregate unclassified corpusAll 249 current baseline files loaded and have 0 mechanical errors; warnings and notes remain distinct hygiene evidencePer-clip intent, loops, variants, and ownership remain incompleteEngine, retarget, contact, visual, gameplay, and cross-pack acceptance not evaluated

Validation-profile status

Validation profileSelectionResult / next evidence
Marketplace intakeselected — observed-pack-capabilityConstituent external inventories and baselines completed; license evidence remains unavailable
Blended locomotionselected — observed-pack-capabilityNo collection runtime sets or engine blend tests were refreshed
Root-motion controllerselected — observed-pack-capabilityArchive-level declarations differ; choose per-clip policy in the target project
State-machine transitionsnot-selectedNo target state-machine contract supplied
Layered upper body/weaponsnot-selectedNo mask or weapon contract supplied
Traversal/environmentnot-selectedNo target traversal contract supplied
Contact actions/interactionsnot-selectedNo target contact contract supplied
Retargeted/customizable charactersnot-selectedNo target rig or retargeter supplied
Motion matching/searchnot-selectedNo search runtime contract supplied
Networked movementnot-selectedNo networked movement contract supplied
Runtime performancenot-selectedNo target hardware/runtime contract supplied

Pack inventory and content evidence

ConstituentFBX filesManifest-declared motions
Basic1210
Female Basic2018
Female1816
Locomotion2018
Longbow2220
Magic2725
Male1816
Pistol/Handgun2927
Rifle 8-Way8381
Total249231

The nine current constituent inventories reconcile to 249 FBX files and 231 manifest-declared motions. The archive member count exceeds each constituent's manifest motion count by two: one non-motion/reference asset per archive variant. Delivered metadata declares 30 fps FBX export preferences, not an engine import result.

Mechanical baseline

Finding/checkAffected scopePotential impactEvidence
No mechanical errors249 filesFile-ready mechanical health onlyobserved-animsmith, AnimSmith 0.10.0; nine fresh empty baselines
duration-sanity warnings50 findingsNo-track assets/clips and channel-end mismatches require semantic classification or reviewobserved-animsmith, AnimSmith 0.10.0
constant-track notes35,405 notes across the corpusHygiene evidence, not authorization to rewriteobserved-animsmith, AnimSmith 0.10.0

AnimSmith remediation evidence

Source issueOperation/declarationsResultIndependent verificationRemaining caveat
Archive-level movement ownership ambiguityCurrent in-place and root-motion XZ controlsIn-place: 0 errors, 13 warnings. Root-motion: 52 stationary-root errors, 37 warningsNine current constituent external runs; control findings are not a source repairPer-clip ownership, controller integration, and all transforms remain outside this rollup
FBX remediationCurrent FBX fix --dry-run representativesSafe refusal: fix accepts only glTF/GLBCaptured exit 2 and stderr in the constituent evidenceNo conversion or transform was authorized

Engine procedures and evidence

RuntimeVersionProcedureObserved resultRemaining gate
UnityunspecifiedNot run at collection scopenot-evaluatedExact import/controller test
Unreal EngineunspecifiedNot run at collection scopenot-evaluatedExact import/retarget test
GodotunspecifiedNot run at collection scopenot-evaluatedExact import/AnimationTree test
BevyunspecifiedNot run at collection scopenot-evaluatedExact loader/graph test

Rig, masking, and compatibility evidence

Pack/rig/set pairSkeleton/retargetScale/axesRoot policyTiming/blendOverall evidence
Nine constituent corpora / target projectMixamo profile resolved for constituent lint onlyNot evaluatedArchive labels do not establish per-clip ownershipNot evaluatedunknown
Constituent-to-constituent pairNo overlapping-path or skeleton/reference-rig comparison refreshedNot evaluatedNot evaluatedNot evaluatedunknown

Limitations and unknowns

  1. This is an aggregation, not a new collection evaluation: no file-to-motion mapping, semantic role classification, runtime set, remediation, engine import, blend, retarget, masking, visual/contact, performance, or cross-pack test was performed.
  2. License terms, target engine, target controller, target rig, and artistic/gameplay acceptance remain unavailable.

Changes between AnimSmith versions

AnimSmith 0.10.0 — The nine constituent reports were refreshed and revalidated with the official evaluator, fresh preflight, current empty baselines, and current archive-variant controls. Current evidence uses output schema v19 and measurements schema v18.

AnimSmith 0.7.0 — Prior collection assertions, output schema v17, measurements schema v16, and retained evidence digests are historical only and superseded by the current constituent evidence.

Reproduction

working-tree state is N/A (official release artifact).

Reproduction used the official release artifact: URL https://github.com/mmannerm/animsmith/releases/download/v0.10.0/animsmith-v0.10.0-x86_64-unknown-linux-gnu.tar.gz; archive SHA-256 8de4f97949fbc61fc3aec1d5f22272735ffe06937a0fea5c998cb3e0f639c662; member animsmith-v0.10.0-x86_64-unknown-linux-gnu/animsmith; binary SHA-256 2052ce64eda53d5037b305561dd0287209719d743b0a4051552e197fbfe4a387. Official tag is v0.10.0, peeled commit db91d8dda3326f97f581d4d62104d928caec383f; working-tree state N/A; compiled features fbx,report; output schema v19 and measurements schema v18.

Required evaluator checks succeeded for --version, top-level --help, inspect --help, measure --help, lint --help, and report --help, and representative FBX admission succeeded. Safe normalized preflight status is preflight-status-v1, SHA-256 739930cd9c04189be3ffe1d3f7381800898d8d4b35c75c9a519e57f6cfad1fad; available help digests are version 35bb24c48d2e2fcf9ae5753338f09722ee50f9c002d00df7d0d30afa5d2ac4a0, top-level 905f42d113783490bc8f3dea6bcda6bfc648a7e2f29b71fec99c41e6701099c8, inspect c45acaabc9e357af7c2c6265a18dccbb67b6cf1cb7dc45263652e0f5fa922c0b, measure cca0b44510c65ee671ac13b591712b6c67f6b1a4f2efdb0aca4ba26293e3d169, lint e216ccd638df639853687025f4eaab35e03c32d306b0dbcd0cb2c5c19ff53def, and report 8298b2c9358bc796362eae5aeae22d86c9a06843c0dec7eb0f06cee82b27ad03. Before exhaustive constituent execution, inspect, measure, lint, and report each exited 0 on representative FBX admission. Each constituent then used external explicit empty-baseline and archive-variant configs; baseline commands exited 0, root-motion controls exited 1 for findings, and FBX fix --dry-run exited 2 because fix accepts only glTF/GLB. Configs, inventories, command outputs, exit codes, and remaining digests are retained outside the repository.

Sources

  • The nine linked constituent reports and external AnimSmith 0.10.0 evidence; no licensed payloads are published.

Rust integration

Embedding animsmith in a pipeline

The CLI is one frontend. Asset pipelines that already own their contract format, importer, build graph, and gate can call the same loaders, measurements, and checks in process.

This guide covers integration decisions. Symbol-level contracts belong in rustdoc, the pipeline scenario guide owns the larger raw-to-game-ready process, and the examples cookbook owns runnable CLI transcripts.

Choose the crates

CrateUse it for
animsmith-coreRequired embedding boundary: data model, rig roles, config, sampling, measurements, diffs, checks, and findings. No file I/O or format dependency.
animsmith-gltfLoad glTF/GLB, write glTF/GLB, or apply byte-surgical glTF repairs.
animsmith-fbxLoad FBX through ufbx; adds a bundled C build. Omit it from glTF-only pipelines.
animsmith-engineResolve one exact built-in consumer/importer profile and fully materialized typed importer settings. No TOML, file I/O, format crate, or engine SDK dependency.
animsmith-reportRender self-contained HTML from the same sampled grids and findings.

The animsmith crate is the CLI binary, not a library facade.

[dependencies]
animsmith-core = "0.14"
animsmith-gltf = "0.14"
# Optional:
animsmith-fbx = "0.14"
animsmith-engine = "0.14"
animsmith-report = "0.14"

docs.rs is the canonical reference for published APIs. The stable package URLs are: animsmith-core, animsmith-gltf, animsmith-fbx, animsmith-engine, and animsmith-report. For the current workspace state, build the same rustdocs locally with just doc.

Rustdoc owns signatures, type invariants, lifetimes, and the Errors and Panics contracts. In particular, start at the animsmith-core crate root; it is the compact API map rather than another copy of this guide.

The compiling end-to-end example is crates/animsmith/examples/embed.rs:

cargo run -p animsmith --example embed

Integration flow

  1. Load a source. Use animsmith_gltf::load_source or animsmith_fbx::load_source when importer-sensitive evidence matters. The immutable result binds the exact primary-file identity and bounded raw facts plus dependency-closure evidence to one normalized Document; borrow them through document(), source_facts(), and dependency_closure(). Calling into_document() explicitly discards both sidecars. Byte-owning hosts with sibling resources use the format crate's explicit-resource-root byte API. The compatibility load_source_bytes/load_bytes entry points do not fall back to the process working directory. The load and load_bytes entry points still return only a Document.

    glTF animation values remain authored; FBX scenes are normalized to metres, right-handed +Y-up coordinates and baked into linear TRS tracks. Raw FBX facts describe parser-projected source declarations and the AnimSmith loader's treatment, not authored key/tangent/interpolation values reconstructed from those baked tracks. Structural failures are loader errors. Semantic defects load and become findings. The same document carries meshes, skins, factor-only materials, and available base-color and normal textures for scene round-trips.

    Raw-fact row sets have explicit complete, partial, or unavailable coverage; a partial prefix proves presence only. Resource rows inventory bounded declarations. The separate closure view maps them to the primary identity, one-read identities for safe rooted sidecars, or typed refusal/unavailability. Each row also exposes its kind-derived loader-essential, nonessential, or target-only purpose; that classification is not an engine-import verdict. Only complete declaration coverage with an identity for every row yields a complete closure identity. The result is exact for that versioned resource domain, not a claim that an unsupported extension or unmodelled FBX domain cannot introduce another target-importer dependency. SourceInfo.path is diagnostic metadata, not identity or raw-fact authority.

    External capture is local and bounded: unsafe, absolute, remote, escaping, oversized, and symlink-mediated locators are not opened or reproduced. Aliases of one normalized source-relative key are read and hashed once; equal bytes under different keys remain distinct dependencies. Path-based loading supplies the trusted root from the primary file. Hosts loading captured primary bytes must explicitly supply a trusted root to resolve sidecars; without one, a loader-essential resource is an error and an optional resource leaves conservative closure coverage.

    An FBX host that needs auditable scale-boundary facts uses animsmith_fbx::load_scale_source instead: the returned wrapper retains the document and FbxScaleCapabilityInventory from one parse. The inventory and normalized source-skeleton sidecar enable only the CLI's narrow rest/bind re-encode path when rest_bind_capability_facts_for_source accepts them; the source-aware form can distinguish known, non-scale-bearing custom properties, resource linkage, and shader/binding-table metadata from an unknown source element. It also admits a BindPose only when it covers every joint of each skin it touches and every exposed converted row is finite, unambiguous, and agrees with the cluster bind or node rest-world matrix the bridge consumes; a source with no Pose remains valid. The same-load source can also prove that enumerated payload omission, influence projection with complete effective coverage, triangulation, and exact-bit welding are conversion-fidelity facts rather than scale-bearing blockers. The inventory retains them, and unknown payload or missing effective geometry/influence coverage still fails closed. The inventory-only rest_bind_capability_facts remains conservative where its frozen aggregate cannot make that distinction. Whole-document FBX scaling remains refused. Complete means the documented ufbx projection covers every representable node/skin identity and joint slot; a missing cluster bone downgrades the sidecar to Unavailable instead of dropping that slot. Unreadable ordered bind declarations retain no finite prefix. The local rests and bind matrices are adjusted/compensated or derived target- coordinate values, not exact authored FBX members. The inventory explicitly records baked curves, rebuilt payloads, omitted authored face/edge members, uninstanced, non-polygon-only, or zero-face mesh definitions (including stable identities for zero-face omissions), and unavailable raw span proof. Stackless source curves are present but unsupported. Retained mesh definitions and source-skin attachments share the stable ufbx mesh identity even when an earlier source definition emits no normalized primitive. A shared FBX geometry remains one normalized definition with multiple compact node instances, so that stable identity stays unique in measurements.

  2. Resolve an optional engine profile. Construct an animsmith_engine::EngineDeclaration with the exact family, profile revision, engine version, importer, and typed document/selector settings. resolve_static validates every declaration without file I/O; then call StaticResolution::resolve_input with the loader-owned SourceFormatV1 and actual clip names. The result contains the immutable facts identity and fully materialized settings identity. Pass that result and the same LoadedSource to project_prediction_provenance_v1; do not reopen or reconstruct either input. There is no generic/fallback profile, caller-supplied source-unit override, or TOML type in this API. If the host has no engine contract, omit projection and retain None; core measurements and checks remain engine-neutral.

  3. Compose one check catalog. Start with animsmith_core::all_checks() and append one borrowed animsmith_engine::EngineAddressabilityCheck::new(&source, provenance.as_ref())?. Pass None when no profile was resolved so the stable engine-owned record remains not applicable. Validate selection against the core ids plus animsmith_engine::ENGINE_CHECK_IDS_V1 before asset I/O, then call evaluate_checks exactly once for the combined per-file catalog. The borrowed catalog intentionally cannot outlive its same-load LoadedSource and provenance. measure_document does not consume either and remains profile-neutral. V1 materializes at most 4,096 actual clip-setting rows; an N+1 document is a typed bounds error rather than a truncated prediction.

  4. Resolve rig roles. Use resolve_configured_roles to apply the same named/auto profile plus inline-override policy as the CLI. Lower-level detect_profile, profile::resolve_named, and ResolvedRoles::from_names remain available when a host intentionally owns a different policy. Checks consume roles, never project-specific bone names.

  5. Build Config. The CLI's TOML is only one constructor. Deserialize the types from your schema or build them programmatically. Deserialization validates numeric check tolerances and per-clip loop caps immediately. MovementOwner represents independent XZ, Y, and yaw intent in core; Config::expectations_for returns canonical effective fields after exact/glob layering, while Config::loop_continuity_tolerances resolves the four effective loop pose and seam-velocity caps through the same per-clip, check-setting, and built-in-default precedence used by the checks. The legacy in_place input maps only to XZ and is cleared from that effective result. A selector that declares both spellings is rejected by Config::validate. Programmatic callers must call Config::validate before passing a directly constructed config to measurement-only APIs; evaluate_checks performs the same validation before inspecting or running the check catalog and returns invalid configuration as a typed EvaluationError::InvalidConfiguration. Config::runtime_nodes is the shared attachment/socket/IK selector policy; the legacy rest-world-scale check field is normalized into it only when the shared field is absent. Supplying both is a typed conflict. This added public field is an intentional pre-1.0 struct-literal break: exhaustive literals must add runtime_nodes or use ..Config::default().

  6. Create one MetricGrids. Share it by reference with measure_document, CheckCtx::new, evaluate_checks, and optional report rendering so each clip is sampled once. animsmith_report::render takes one ReportInputs carrying those grids, the roles, the evaluations and the same Config the checks ran under; the configuration is what lets it draw a declared gait group's members against each other, since a coherent group emits no finding naming them.

  7. Map results into the host. Finding carries a stable check id, severity, optional clip/bone/time, measured and expected values, and a message. The host decides whether warnings fail its gate.

Call evaluate_checks with the full catalog and a CheckSelection. It returns one CheckEvaluation per catalog check, including disabled, unselected, not-applicable, partial, and not-evaluated work. CoverageGap::code and EvaluationScope::code are the machine fields; never reconstruct coverage by parsing a message. Content findings are nested under their owning check and coverage gaps are never encoded as findings.

Role resolution remains an explicit frontend step. Use resolve_configured_roles, CheckCtx, and Config::rig rustdocs for the exact profile, override, and unresolved-role contracts.

Compose the outputs you need

An embedded gate does not need to reproduce every CLI output. It can emit clip measurements, add static asset measurements, run findings, render HTML, or combine those results with host-owned checks. Share the same MetricGrids within the limits documented by its rustdoc so those consumers judge one sampled representation.

The MetricGrids, measure_document, and measure_assets rustdocs own cache thread-safety, sampling, static-domain scope, and identity details. Primitive::additional_influence_sets and the corresponding mesh measurement records expose secondary glTF skin-attribute presence only. Mesh records also preserve whether either side was unpaired on an individual primitive; they do not retain or evaluate secondary per-vertex payloads.

For glTF/GLB, asset measurements additionally provide source-order material, texture, and image inventory records plus explicit material_resource_coverage. Complete glTF/GLB coverage is scoped to the five documented core bindings, in order: base_color, normal, metallic_roughness, occlusion, and emissive. It does not cover extension-defined texture slots. These are loader observations, not a portable image-management API: a host must handle "unavailable" coverage from other loaders and must not infer writer preservation, image acceptance, repair, resizing, transcode, color-space policy, or engine-import behavior from them. Use the typed image metadata to inspect declared MIME separately from the byte-detected container and decoded color/channel data; unavailable images carry a reason instead of decoded metadata.

When the host needs to exchange the same JSON as the CLI, construct MeasurementContract, MeasureFileReport/LintFileReport, and MeasureEnvelope/LintEnvelope from animsmith-core::contract. That module owns both immutable URNs and derives the lint/measure summary from the supplied records. It also exposes the typed MeasurementReportInput subset and MeasurementReportFile records for consumers that need to validate and recover every file's exact display path and full measurement contract from a current measure or lint report. The core boundary preserves file order and accepts empty or multi-file reports; each consumer owns any cardinality rule for its workflow. MeasurementReportInput::file_count lets an adapter retain the raw record count before into_files consumes and validates the report, while MeasurementReportError::File carries a typed MeasurementFileError and an index (also available through file_index) without adding consumer-specific prose. The compiling example emits a full schema-valid lint envelope; embedded producers do not need to copy private CLI structs or hard-code protocol identities. Host-specific sidecars remain appropriate when CLI interoperability is not needed.

Gate and stability contracts

The CLI convention is a useful default for an embedded gate:

  • no error findings and no required-unavailable prediction facets: success (warnings and ordinary coverage gaps may remain visible);
  • any Severity::Error: content rejection;
  • any required_prediction_unavailable facet: prediction-evidence rejection, regardless of severity or allow-list policy;
  • loader/config/I/O error: operator failure, kept separate from findings.

Missing prerequisites are typed coverage gaps, and disabled/unselected checks remain visible without executing. Severity overrides apply only to content findings. Ordinary coverage gaps are nonblocking by default; the embedding host owns any stricter required-check or release-lane policy. To match the CLI's finding threshold, allow-list, and unsuppressible prediction policy, call animsmith_core::lint_requires_failure on the completed evaluations rather than deciding from findings alone.

Built-in scope and gap codes may be emitted only by the checks declared in the core evidence-code authority; CheckEvaluation::evaluated returns a typed error when a different check claims one. Embedded checks retain an open vocabulary by using namespaced custom codes such as acme:input_unavailable. The public built-in-code slices are available to consumers that need to enumerate or allow-list animsmith-owned codes.

For the current pre-1.0 API, prefer the crate-root flow: loader → role resolution → Config → MetricGrids → measurements/checks → findings. The durable automation contracts are deliberately narrower than the pre-1.0 Rust API:

  • built-in check ids used by config and findings;
  • CLI exit codes and the versioned JSON envelope, when the host interoperates with the CLI.

The animsmith-core crate root owns the full API status contract. Rust symbols, model and transform types, metric formulas, and diff thresholds may still be refined before 1.0. Match #[non_exhaustive] result types with a fallback arm. The Check trait supports experiments with custom checks, including opt-in checks that override enabled_by_default; an explicit "note", "warn", or "error" setting activates one. There is no stable plugin registry yet; wrapping animsmith findings with host-owned checks keeps that boundary explicit.

Migrating an existing pipeline

For a command-by-command migration plan, use the pipeline scenarios for marketplace intake, mocap cleanup, outsourced acceptance, CI gating, and raw/generated artifact storage, then use the cookbook for exact commands.

For the library cutover itself:

  1. Capture accepted measurements from the old pipeline as golden values.
  2. Run the CLI and embedded path from the same Config until their findings agree.
  3. Compare old and new measurement maps with diff_measurements; judge motion deltas rather than binary file differences.
  4. Keep project-specific sidecars, hashes, provenance, and storage policy in the host pipeline.

Foot-cycle map planning

animsmith_core::plan_foot_cycle_parameterization_v1 is the pure planning boundary for a manifest-declared in-place gait ring. A host supplies the strict typed parameterization, its exact byte identity, the already validated collection manifest and exact byte identity, plus canonical contact fragments and independently measured typed Root/Hips in-place evidence bound to each fragment's exact artifact, dependency closure, and selected collection take in declared member/path order. The planner validates manifest clip witnesses, the known stance-detector provenance and exact common detector threshold, bounded aggregate canonical-fragment work, alternating bilateral support topology, positional boundary correspondence, and every declared segment-slope bound. The strict declaration also carries a required exact proof policy: finite maximum gait-phase spread and contact-boundary phase error in [0, 0.5], plus a finite non-negative minimum left/right amplitude. The plan retains that policy together with the identity of the exact parameterization bytes; it does not obtain defaults from config or merge values across members. It returns one exact ContactTransformBindingV1 and duration-preserving ContactTransformOperationV1::TimeWarp per member.

After the host has independently bound the selected loaded clip to that exact member plan and bound every track bone index to its selected, validated skeleton, animsmith_core::time_warp_clip_v1 can build a pure cloned clip candidate. The map is source-to-output: LINEAR authored keys move forward and interior map knots are sampled with AnimSmith's normal runtime semantics; STEP tracks move only their authored breakpoints. Unsafe nonconstant CUBICSPLINE tracks and binary32 time collisions refuse the whole candidate. A successful candidate still carries no emitted-artifact identity or reread proof. animsmith_core::time_warp_rows_v1 yields one FootCycleClipWarpRowV1 per emitted key — authored, a knot of its own, or the two coalesced — and FootCycleClipWarpKnotV1 carries the one rule that decides when a control point and an authored key are the same emitted instant, so an embedder proving a candidate independently can ask those questions instead of restating them. A row says which key exists and where in the order, never what it stores: the knot it names carries its control point's index, so a proof narrows every stored time itself instead of trusting the producer's resolution. animsmith_core::preflight_time_warp_clip_v1 runs the same validation and returns exact candidate name/track/key/value/storage-byte counts plus a conservative V1 inspection-work charge without allocating the candidate, allowing embedders to enforce their own checked aggregate batch budget before retaining multiple outputs.

The planner does not open paths. The CLI crate has a private adapter that validates the closed bounded TOML grammar, resolves the manifest and parameterization path sets with symlink/canonical and platform-observable hardlink alias refusal, validates the pure runtime-set binding before host source access, reloads only its exact member-reachable sources/configs, retains their normalized config semantics and byte identities for proof without rereading controls, derives the typed in-place root evidence, cross-checks plans against exact selected takes, and prepares bounded clip candidates. Root evidence retains signed endpoint X/Z and signed accumulated yaw even though the unchanged in-place gate applies horizontal hypot(X, Z) and absolute yaw. The prepared collection also retains exact aggregate source metric pose-cell and sample-evaluation totals for the shared source/output proof budget. It also runs the closed operation-aware stance-detector extension handler, but retains contact transformation as a typed continuation: only a serializer supplies a truthful fresh output artifact identity and dependency closure. The private in-memory continuation now serializes every candidate as a strict self-contained GLB, rereads the exact bytes, and independently proves the transformed clips and contacts as one all-or-nothing batch. The public CLI then bounded-serializes member and aggregate evidence and publishes the complete fixed generation through one no-replace directory rename. Embedders must not treat a successful plan or prepared candidate as an output-artifact or gameplay-acceptance proof. See the collection contract for the declaration and exact topology grammar.

The format-edge candidate writer is also a private continuation, not a producer: animsmith_gltf::write::preflight_glb_bytes records one exact in-memory projection receipt and write_glb_bytes reruns it before retaining bytes. StrictFootCycleV1 is deliberately narrower than legacy conversion and refuses data it cannot represent exactly enough for the foot-cycle candidate contract, including source scene collapse, incomplete skin bindings, primary skin-slot reinterpretation, unsupported primitive/texture/material domains and non-empty complete source material-resource sidecars, and unrepresentable complete source-node or source-skin facts. The receipt binds emitted JSON/BIN projection bytes, not deliberately excluded source-only evidence. It preserves no source-only names, stable source indices, or image-inspection/provenance sidecars as output facts. An exact complete-empty material resource graph is admitted only when the writer-facing material list is also empty. The collection proof clones each prepared source document, replaces only its selected clip, preflights all candidate bytes and shared metric-grid work before writing, rereads the exact GLB bytes, requires a complete closure with no external resources (embedded-buffer references may target Primary), finishes the contact transform with the reread identities, and independently checks duration, map/contact correspondence, gait, signed root trajectory, and configured loop pose/velocity tolerances. The CLI-only transaction owns generation staging/publication; the format crate does not. Neither path claims game-engine acceptance.

Scale plan and proof contracts

The scale workflow is the operator-facing guide. Normative algebra, tolerances, refusal boundaries, and ownership live in DESIGN.md Appendix D; the output reference owns the evidence wire contract.

For an embedded producer:

  1. Preflight the exact source format with animsmith_gltf::preflight_scale_source, then project it with animsmith_gltf::capability_facts into ScaleCapabilityFacts.
  2. Call plan_scale once with an explicit ScaleOperation. The opaque plan owns the operation parameters and a numeric-free structural ledger.
  3. Apply that same plan to the exact source representation. For glTF/GLB, rewrite_scale_plan cross-checks raw hierarchy and container identities against the plan before changing JSON or accessor bytes.
  4. Reload the emitted artifact, wrap the document with ScaleCandidate::from_document, and call prove_scale with the original source and same plan.
  5. Run the format artifact proof as well. Core proof covers normalized semantic claims; artifact proof covers exact raw preservation, container structure, declared bounds, aliasing, deterministic bytes, and the actual write set.
  6. Publish only the proved artifact and matching evidence as one coordinated pair, preserving the CLI's rollback and documented process-crash semantics.

Those producer steps apply directly to glTF/GLB. With the default FBX feature, the narrow rest-bind producer first requires a complete normalized FBX inventory, stages a private GLB, and then runs the same raw-GLB plan/rewrite/ proof transaction; whole-document remains unsupported for FBX. The FBX inventory is not authority to claim raw FBX spans, object properties, curve keys, material/texture assignments, or vertex identity were preserved. The source-aware gate may admit bounded texture/video declarations and discarded user-defined properties while retaining their raw facts and closure evidence; all incomplete or unknown scale-bearing domains still refuse with the exact failed fact or counter.

ScaleCandidate grants no authority: proof independently validates the source, plan inventory, candidate structure, and numeric claims. A frontend must not substitute a normalized writer for exact-source rewrite when the operation promises preservation of unknown or format-only payload.

ScalePlan::ledger() exposes read-only field, payload-shape, source-topology, and proof-claim views for adapters. These rows name structural ownership and identity, not resolved multipliers or expected numeric values. Writer and proof may share this numeric-free binding vocabulary, but each must derive its own numeric expectations. Under unavailable source coverage, best-effort raw locals remain compatibility output rather than replay identity.

Each ScaleProof claim is a read-only ScaleProofResidual carrying its maximum and comparison count together. evaluated() distinguishes a measured zero from an obligation that walked nothing. The fixed ScaleTolerancePolicy::APPENDIX_D_V6 identity also owns the sampled-work budget; proof refuses over-budget input rather than proving a subset.

The glTF convenience functions rewrite_linear_units and rewrite_rest_bind compile and delegate to the same plan-taking adapter. Use the plan-taking boundary when the host already owns the plan. The format frontend retains raw capability, hierarchy, component, type/count/range, alias/overlap, and complement checks that core cannot express.

See the public rustdoc for exact selectors, row variants, errors, and proof fields. Calibration populations and historical timing notes are intentionally separate in scale-calibration.md.

What the libraries do not own

  • Parsing the host's contract files into Config.
  • Hashing assets, tracking provenance, or deciding staleness.
  • Choosing raw/generated artifact paths or retention policy.
  • Artistic retargeting, contact cleanup, or motion editing.

Those decisions surround animsmith in the raw-to-game-ready pipeline; they are not API responsibilities.

docs.rs API references

This reference is published outside the AnimSmith Pages site.

Open docs.rs API references

Project and contributing

  • CONTRIBUTING.md — Work on animsmith itself — contribution flow.
  • DEVELOPMENT.md — Set up a checkout and run the local verification commands.
  • RELEASING.md — Cut a release — the release-plz flow, the manual 0.1.0 bootstrap, and the published-doc-link policy.
  • SUPPORT.md — Get help or file a bug.
  • SECURITY.md — Report a vulnerability privately.
  • DESIGN.md — Follow the architecture, check-catalog rationale, and roadmap.
  • Scale proof calibration — Review the implementation-owned calibration sweep, provenance magnitudes, historical policy measurements, and reproducible command behind appendix-d-v6.
  • research/ — Read the dated research notes that inform the roadmap.

Contributing

Thanks for improving animsmith. This project is pre-1.0, so small contract changes can still happen, but every user-visible change should be explicit, tested, and documented.

Pull Request Flow

All changes land through pull requests. Keep each PR focused on one behavioral or documentation goal, and prefer follow-up issues over expanding a PR after review has started.

Before coding, state the user outcome, invariants, non-goals, and simplest maintainable end state if the change were designed today. Inspect related issues and existing authorities before adding a new concept. Focus means one coherent outcome, not the smallest diff; prefer a larger coherent refactor over preserving duplicate authority.

Use this lifecycle for non-trivial changes:

  1. Start from current main.
  2. Implement the change with behavioral tests when behavior changes.
  3. Update docs in the same PR whenever public behavior, commands, output, feature flags, or supported workflows change.
  4. Run the required local gates from DEVELOPMENT.md.
  5. Open a draft PR with a description of the behavior change, the chosen design, verification performed, and known out-of-scope work.
  6. Address review findings and run the project audit checklist for substantial changes.
  7. Leave the PR for a maintainer merge decision.

Trivial documentation-only changes can use a shorter path, but they still need a PR, a conventional title, and enough verification to show the rendered links and affected files are correct.

Conventional Commits

Every PR title and every non-merge commit that lands on main must use Conventional Commits. CI enforces the accepted types from .commitlintrc.yml:

feat fix perf revert chore ci docs style refactor test build

Use docs: for documentation-only changes. Use feat:, fix:, or perf: only when the commit should affect release notes and version calculation. Release automation groups the merged conventional commits into the changelog.

Agent-authored commits also need the agent attribution trailer required by that agent's local instructions.

MSRV Policy

The minimum supported Rust version is a floor, not a ratchet. It moves only when something in the tree actually requires a newer compiler — normally a dependency raising its own rust-version, occasionally a language feature the code has a concrete reason to adopt. It is never raised merely because a newer stable exists.

The ceiling on that movement is six months: a bump must not require a compiler released within the last six months, so consumers who pin toolchains always have a two-release window to catch up. Six months is stated in months rather than as N-minus-releases because months are the unit downstream users experience, and the rule then survives a change in Rust's release cadence.

Day-to-day development still happens on current stable, and CI checks stable on Linux, macOS, and Windows. The MSRV is what the published crates promise, not what maintainers run.

workspace.package.rust-version in the root Cargo.toml is the single source of truth. Nothing else states the number independently: the CI MSRV job reads it out of the manifest, and two gates hold the prose to it (see the Toolchain section of DEVELOPMENT.md). A bump is therefore a one-line manifest edit plus whatever prose the gates flag.

Because an MSRV bump is a compatibility change for downstream crates, it warrants a minor version rather than a patch, and a changelog entry saying so.

Workflows must not pin dtolnay/rust-toolchain to a version-shaped ref such as @1.88. Dependabot reads those as action tags and bumps them, which silently rewrites the Rust version — it once proposed @1.100, a nightly number, for the MSRV job. Use a branch ref (@stable, @nightly) or a SHA pin with an explicit toolchain: input; scripts/check-github-community-files.sh enforces this.

Documentation Freshness

The PR description must call out documentation impact. If a change affects user-visible behavior or public contracts, update the relevant docs in the same PR or link a follow-up issue labeled type:docs.

Documentation impact includes:

  • CLI commands, flags, exit codes, or examples.
  • Machine-readable JSON output or schema ids.
  • Public Rust symbols, crate features, loader boundaries, or README claims.
  • Task guides, tutorials, example projects, and current version or project-status claims.
  • Check ids, severities, thresholds, config keys, or measurement semantics.
  • Release, support, security, or contributor workflows.

Review the complete affected stakeholder journey, not only files named by the issue or touched by the implementation. Search the root and crate READMEs, docs/, examples/, rustdoc, and release/process docs for superseded commands, identifiers, schemas, versions, examples, and status claims. Preserve clearly historical references.

Mechanical freshness checks cover the parts that are cheap and reliable: schema URL consistency, GitHub community files and PR-template coverage, crate README/package inclusion, docs.rs manifest metadata, and rustdoc missing-docs enforcement. Semantic docs freshness remains a review requirement: update docs in the same PR as the behavior change, or link a focused type:docs issue.

During pre-1.0 development, published package READMEs intentionally link deeper repository docs to latest main with github.com/mmannerm/animsmith/blob/main/... URLs. If a release later needs version-pinned README links, update RELEASING.md and the package-readiness gate in the same PR.

Do not duplicate durable process rules across multiple files. This file owns contributor process. DEVELOPMENT.md owns local setup and verification commands. Agent files may add agent-specific deltas, but should link back here for the shared process.

Audit Expectations

Run the project audit checklist before asking for a merge on substantial changes: new checks, subcommands, measurement semantics, output contracts, dependency additions, write/fix path changes, or broad docs restructures.

Current-base gate

Immediately before final audit and readiness, fetch and record the review base: origin/main for a normal PR, or the declared parent branch for an explicitly approved stack while its parent is open. Record the candidate HEAD and fetched review-base SHA, then prove that base is an ancestor of HEAD. If the base has advanced, rebase onto it or otherwise integrate it, then repeat the final diff review, local gates, exact-head CI, and audits. Any change to HEAD or the recorded review-base SHA invalidates that readiness evidence. After a stacked parent merges, retarget the PR to main, rebase onto current origin/main, and repeat this gate.

Substantial agent-authored changes also require one reciprocal cross-model CLI audit: Codex invokes Claude, and Claude invokes Codex. The versioned audit workflow owns reviewer selection, model/effort, session reuse, attribution, and exact-head evidence rules.

The audit should check:

  • Simplicity: the design is scoped to the issue and avoids needless abstractions.
  • Tests: behavior changes have focused coverage and the local gates match CI expectations.
  • Invariants: crate boundaries and dependency rules still match DESIGN.md.
  • Documentation: README files, docs, and PR text stay fresh.

Follow-up work found during audit should become GitHub issues rather than TODO comments. Search existing issues first, then file a focused issue with the right type:* and priority:* labels.

Labels And Milestones

Use type:* labels to describe the work area:

  • type:bug for incorrect behavior.
  • type:feature for new capabilities or enhancements.
  • type:docs for documentation, examples, and tutorial gaps.
  • type:refactor for behavior-preserving code structure changes.
  • type:chore for maintenance work.

Use priority:high, priority:medium, or priority:low when priority is known. Put pre-release work in the active release milestone unless a maintainer chooses another milestone.

Merge Policy

Maintainers merge PRs after review, verification, and any required audit are complete. animsmith uses merge commits so every branch commit lands on main with its own conventional subject; do not rely on squash merge to repair commit history after review.

Development

This file is the canonical local setup and verification guide for a source checkout. Contributor process lives in CONTRIBUTING.md; release operations live in RELEASING.md; architecture and crate boundaries live in DESIGN.md.

Toolchain

animsmith uses the Rust 2024 edition and MSRV 1.88, declared in the workspace Cargo.toml. CI checks stable Rust on Linux, macOS, and Windows, plus the MSRV on Linux.

Develop on current stable; the MSRV is the floor the published crates promise, not the compiler maintainers run. When that floor may move is governed by the MSRV policy in CONTRIBUTING.md.

workspace.package.rust-version is the single source of truth for the number. The CI MSRV job reads it out of the manifest rather than pinning a second copy, and two gates hold the prose to it: this file's MSRV sentence via scripts/check-github-community-files.sh, and every crate README and rustdoc header via crates/animsmith/tests/msrv_docs.rs. A bump is therefore a one-line manifest edit plus whatever prose those gates flag.

That same shell gate also rejects a version-shaped dtolnay/rust-toolchain@1.88 ref in any workflow, because Dependabot rewrites those as if they were action tags.

Install the local tools used by the gates:

$ just install-rust-tools

That installs sccache, cargo-deny, typos-cli, and cargo-llvm-cov if they are missing. Cargo still works with stock defaults.

The animation-pack skill gate also uses Python 3, PyYAML 6.x, and JSON Schema 4.x to validate skill metadata and the closed evaluation-model schema:

$ python3 -m pip install "PyYAML>=6,<7" "jsonschema>=4.18,<5"

sccache

Local sccache is optional. To configure a user-level Cargo rustc-wrapper for faster repeated builds across worktrees:

$ just configure-sccache

Use RUSTC_WRAPPER= on an individual command when you intentionally want to bypass sccache.

CI uses GitHub Actions caching and Swatinem/rust-cache; it does not depend on a private runner cache.

Common Commands

$ just build
$ just test
$ just doc
$ just animation-pack-skill
$ just gates

just gates is the local PR gate and should be green on each candidate head before pushing a non-trivial PR. It runs formatting, clippy, workspace tests, golden skip marker verification, dependency checks, schema-id verification, GitHub community-file checks, spell checking, rustdoc with missing public docs denied, no-default-features CLI tests and builds, the retained release CLI proof, package readiness checks, and the animation-pack skill's behavioral and published-report validation.

This author-side pre-push gate is distinct from the later PR audit. Once the same commit is pushed, audit agents reuse the captured local result and the PR's required checks; they do not rerun just gates independently.

The corresponding CI workflows also validate the same expectations on a clean checkout. Coverage and the security scanners (Scorecard, CodeQL) run only in CI and are informational, so they are not part of the local gate (see below).

Pull requests also receive one separate checks / animation-pack result from the reusable checks workflow. It runs the same animation-pack validator suite once on the exact pull-request head, equivalent to the local just animation-pack-skill recipe, covering the versioned skill validators and every maintained report/appendix pair. Audit agents should retain and reuse that exact-head result instead of rerunning the suite independently. The check uses only repository-safe fixtures; licensed animation packs remain outside the repository and CI.

no-default-features

The default CLI build includes FBX support through ufbx and the HTML report feature. The --no-default-features build must keep working as a pure-Rust glTF-only binary:

$ cargo test -p animsmith --test cli_contract --no-default-features --target-dir target/no-default-features
$ cargo test -p animsmith --test foot_cycle_cli --no-default-features --target-dir target/no-default-features
$ cargo test -p animsmith --test scale_cli --no-default-features --target-dir target/no-default-features
$ cargo test -p animsmith --test transition_pose_cli --no-default-features --target-dir target/no-default-features
$ cargo build -p animsmith --no-default-features --target-dir target/no-default-features
$ cargo build -p animsmith --release --no-default-features --target-dir target/no-default-features

--target-dir is not optional bookkeeping. Without it these builds land on target/debug/animsmith, target/release/animsmith, and target/doc, and whichever feature set ran last is what stays there; see Evaluation Artifacts below. just github-community enforces the pairing: in justfile, .github/workflows/checks.yml, and scripts/*.sh, a non-comment line naming --no-default-features must name --target-dir target/no-default-features on that same line.

In that build, glTF inspect, measure, lint, transform, fix, scale, diff, generate addressability, and collection transform-foot-cycle stay available, as do evaluate-transition-poses and collection evaluate-transition-poses. HTML reports require the report feature; FBX input, convert, and assemble require the fbx feature.

scale and collection transform-foot-cycle are the minimal build's evidence-emitting producers, so their shared atomic artifact/evidence and generation publication helpers live in crates/animsmith/src/publish.rs rather than inside the feature-gated assembly module. Both scale_cli and foot_cycle_cli are part of the no-default-features gate.

Evaluation Artifacts

A checkout holds more than one CLI, and they do not have the same capabilities. Every --no-default-features command carries --target-dir target/no-default-features, so no feature-variant build writes to the conventional paths:

PathCompiled featuresUse
target/release/animsmithfbx, report, proven by probingexternal evaluation, after a green just gates
target/debug/animsmithdefault: no feature-variant build writes heredevelopment and the local skill gates
target/no-default-features/{debug,release}/animsmithnoneproving the minimal build still works
a published release archivefbx, reportexternal evaluation, verified per RELEASING.md

The first row is the only one this repository proves rather than arranges, and just release-cli is what proves it. It builds nothing, so it judges the artifacts a run leaves behind: both binaries must admit glTF, the retained target/release/animsmith must also admit the self-authored FBX fixture and expose report, and the isolated minimal binary must refuse those two. It then prints a provenance record to keep beside an evaluation's results — the binary path, its SHA-256, its --version line, the capabilities the probes proved, git rev-parse HEAD, and git describe --tags --dirty. That last field reads no tag reachable in a shallow or tag-less checkout, which is what CI clones; it is not degraded to a bare hash, because a hash there would read like a release name while naming none.

just gates runs it as its last line and the CI test job as that job's last step, so what it attests to is what the completed run retained. The isolation itself is held in place separately, by just github-community: across justfile, .github/workflows/checks.yml, and scripts/*.sh it requires the two flags to appear together on one line, and it requires the probe to be the last step of the gates recipe and of the workflow's test job, so nothing runs afterwards that could replace the artifact it just attested to.

Cross-platform determinism

Committed example assets and documentation visuals are byte-compared, and CI runs that comparison on Linux, macOS, and Windows, so every number that reaches those bytes has to be identical on x86-64 and arm64.

Model-space sampling is where that bites. glam is pinned to its scalar-math and libm features in the workspace Cargo.toml, so the FK that builds a pose grid uses scalar Rust f32 arithmetic and the libm crate's transcendentals rather than SIMD intrinsics and the platform's own libm. Rust never contracts a * b + c into a fused multiply-add, so scalar IEEE-754 arithmetic gives bit-identical results everywhere; glam's NEON path on arm64 does fuse, and its SSE2 path on x86-64 does not, which moves the last bits of every matrix product. A committed report embeds the sampled grid as base64, so those last bits are the file.

Fixtures whose motion is translation-only never saw this — adding exact values is bit-identical on any IEEE platform. The first committed report whose model positions come from a rotation (clip-dirty.report.html) is what surfaced it, as a macOS-only failure of committed_visuals_match_the_generator_output.

Prefer fixing determinism at the source over loosening a byte comparison: the comparison is what makes a committed picture evidence.

The one thing that byte comparison cannot answer is what changed across a release: the glTF writer stamps animsmith <version> into asset.generator, so every committed example asset changes when the version does. PAYLOAD_IDENTITIES in crates/animsmith/tests/examples_cookbook.rs pins each asset's content around that stamp, so a release refresh leaves the pins alone and a changed animation does not.

Golden Tests

Golden tests include an env-gated reference test against licensed assets and CI-visible FBX mesh/skin/clip coverage from self-authored checked-in fixtures. The reference test skips cleanly when ANIMSMITH_GOLDEN_GLB is unset and prints the grep-able marker ANIMSMITH_GOLDEN_SKIP; CI and just gates assert that marker is present.

$ ANIMSMITH_GOLDEN_GLB=/path/to/reference-character.glb just golden

ANIMSMITH_GOLDEN_GLB is a developer-local, one-time reference input. Never commit it or expose it through CI downloads, secrets, caches, logs, or artifacts.

The committed-fixture rule applies repository-wide, not only under testdata/: use synthetic/self-authored assets, CC0 assets, or other assets whose license explicitly permits repository inclusion and CI use/redistribution. Record provenance and license evidence for every non-synthetic fixture. Commercial and other redistribution-restricted assets, including excerpts and motion-bearing derivatives, stay outside the repository and CI.

The same boundary governs what tooling may publish about a licensed clip. Published evidence is limited to summary numbers, non-recoverable digests, scrubbed labels, and single-series metric charts — one or two scalar series per clip, such as foot height or root speed. Per-bone pose data is a motion-bearing derivative and is never published, and that includes the HTML report's sampled pose grid: the grid is the model-space position of every bone on every judged frame, so a full report of a licensed clip carries the clip. The artifact form that satisfies this rule is animsmith report --evidence-only, which is what may be attached to an issue, published, or sent to a vendor.

Documentation Builds

Use just doc for rustdoc warnings-as-errors and missing-docs enforcement. It renders workspace docs and the CLI crate without default features with -D warnings -D missing_docs, so all six publishable crates keep documented public surfaces.

When editing public docs, cargo test --workspace validates Markdown link targets and #anchors in the gated doc set (the docs_links test); review GitHub forms and rendering by inspection. The root README.md is also the crates.io front page for the animsmith CLI crate, so keep its links absolute and keep CLI-user content first.

The release_version_docs workspace test also keeps the current dependency snippets in the published READMEs and embedding guide aligned with the current tool.version examples. It accepts the manifest version or exactly its next patch or minor during pre-dispatch documentation staging; on a generated release-plz-* branch, every current version claim must exactly match the bumped workspace manifest. Historical changelog, bootstrap, and roadmap versions are deliberately outside that inventory.

Those claims are written rather than hand-edited:

$ cargo run -p animsmith --example stage_release_docs
$ cargo run -p animsmith --example stage_release_docs -- --version <next>

With no argument the target is [workspace.package] version; --version stages the next patch or minor before the release workflow is dispatched, and any other version is refused. The inventory and the reader that locates each claim live in animsmith-testkit's docs_versions module, which both the example and the test drive: validating is asking whether staging would report a change, so the tool rewrites exactly the spans the gate reads and a version-shaped string elsewhere on the same page is not a current-version claim. A second test holds that inventory to the tracked Markdown, so a new snippet cannot be added without entering it. Re-running the example changes nothing, and the gate — not the example — is what fails a stale checkout.

GitHub Pages preview

The Pages site uses the mdBook version pinned in .mdbook-version. Install it locally before using the Pages commands:

$ cargo install mdbook --version "$(tr -d '[:space:]' < .mdbook-version)" --locked

just docs-check stages tracked repository files outside the checkout (in a deterministic sibling directory named after the checkout by default), generates the book, builds it, parser-validates the staged Markdown destinations, and rejects rendered local links without targets in the built artifact. just docs-serve stages the same source and serves it locally. Set ANIMSMITH_DOCS_STAGE to choose another external staging directory; it must not overlap the checkout and must not be committed.

Navigation comes from the Category column of docs/README.md. A cell is either Part or Part › Group: parts become mdBook part titles in first-appearance order, and a group becomes a generated chapter at _generated/groups/<slug>.md listing its member rows with their Use it to… text. Staging fails when a part or a group is split by another one, when the same group name appears under two parts, when two group names collapse to one slug, or when a group name has no slug characters, so the sidebar always follows the table's order. The row pointing at docs/reports/README.md nests the report and evidence pairs from that table below itself. Chapters with children start collapsed ([output.html.fold] at level 0).

Staging also resolves the media a page embeds: an <img> naming a tracked drawing under docs/visuals becomes that drawing's own markup, so it reads the page's theme tokens instead of guessing light or dark from the operating system, and an <iframe src> becomes a site-absolute path, which mdBook never rewrites and which the aggregated print page therefore needs. The repository Markdown keeps the plain references GitHub renders.

Tracked docs/site files stage as mdBook's theme/ override directory instead of as book source, so the stylesheet, fonts, and favicons style the site without becoming pages. docs/site/animsmith.css is required and wired as additional-css; staging refuses a checkout that does not track it rather than publishing an unstyled book. docs/site/redirects.toml is configuration rather than a page: each "/route.html" = "relative/target.html" entry becomes an [output.html.redirect] route, and the build fails when a redirect target is missing from the artifact. Built README.md chapters receive compatibility routes because mdBook renders them as index.html. Generated links to repository source are pinned to the selected release tag at the release root and to main below /dev/.

Spell Checking

just gates runs typos over source, comments, and docs. Run it alone with:

$ just typos

Domain jargon that reads as a misspelling (and binary DCC fixtures that embed vendor strings) is allow-listed in _typos.toml. Add new project terms there rather than rewording correct code.

Coverage

Line and region coverage come from cargo-llvm-cov (source-based LLVM instrumentation). Generate a local HTML report under target/llvm-cov/html:

$ just coverage

CI runs the same tool and uploads the lcov report to Codecov, which renders the README badge and per-PR diff coverage. Coverage is informational and never blocks a merge; the codecov.yml project and patch statuses are set to informational. Enabling the repository on Codecov and adding the CODECOV_TOKEN secret is a one-time maintainer step (see .github/workflows/coverage.yml).

Security And Supply-Chain Scans

Beyond cargo audit and cargo deny (the audit workflow), CI runs three informational security scans that report to the GitHub Security tab and are not wired into branch protection:

  • OpenSSF Scorecard grades repository security posture weekly and backs the README badge.
  • CodeQL performs static analysis of the Rust sources on main pushes and weekly, keeping results in the Security tab without adding another full-workspace compile to every PR.
  • Dependabot (.github/dependabot.yml) opens weekly PRs to bump Cargo dependencies and pinned Action versions. Minor and patch Cargo bumps and Action updates are grouped into one PR each; major Cargo bumps open individually so each breaking upgrade is reviewed on its own.

Package Readiness

The package inventory gate protects the publishable crate contents intended for crates.io:

$ just package-inventory

It verifies each publishable manifest's docs.rs metadata and README choice, checks that package inventories include the README, manifest, and source entry point, and fully verifies animsmith-core with cargo package. The shared include list carries workspace license and notice files. The dependent crates cannot run full cargo package verification until the matching animsmith-* dependency versions have been published to crates.io, so the local gate uses cargo package --list for them and the release flow publishes in dependency order.

When the matching internal dependency versions already exist in the registry, also dry-run package assembly for any affected dependent crate:

$ cargo package -p animsmith-gltf

The release binary workflow packages CLI archives and detects the CLI release tag through shared scripts (scripts/package-release-binary.py, scripts/select-cli-release-tag.sh) so both are exercised locally rather than only in CI:

$ just release-packaging

Bevy Readback Lock

The isolated exact-Bevy probe under tools/bevy-readback is excluded from this workspace and owns its own Cargo.lock. That lock is the single repository-owned authority for the resolved graph the probe observes: the probe hashes it with include_bytes!, and the engine's strict reader rejects any readback whose recorded lock identity differs.

The engine cannot include a file from outside its published crate, so it carries that identity in crates/animsmith-engine/src/bevy_readback_lock.txt: two lines, the byte count then the lowercase SHA-256, read with include_str! and parsed at compile time into the crate's two public constants. Do not edit that file by hand; rewrite it from the lock:

$ just bevy-readback-lock-refresh

just bevy-readback-lock (part of just gates) renders those same two lines from the committed lock and fails when they differ from the committed file, printing the identity the lock renders, the refresh command, and a diff. It also checks that the lock pins animsmith-core and animsmith-engine at the workspace version and every Bevy 0.19 release crate at 0.19.0. Its self-test covers the identity-file comparison and the refresh that repairs it, on temporary copies; the version and Bevy-patch pins are exercised by the opt-in matrix below, which has a lock fixture for them.

The opt-in compile and runtime matrix for the probe itself needs Rust 1.95.0 and is not part of CI:

$ just bevy-readback-test

Releasing

Releases are automated with release-plz. You never hand-edit versions: when a maintainer manually dispatches the release workflow, release-plz opens or updates a release PR that bumps the shared workspace version, propagates the internal animsmith-* dependency versions, and updates CHANGELOG.md. Merging that PR tags the release, publishes the GitHub Release, and publishes the workspace crates intended for crates.io in dependency order.

The workflow is .github/workflows/release-plz.yml; its behaviour is configured by release-plz.toml. The changelog uses release-plz's default Keep-a-Changelog format, derived from the Conventional Commit history (accepted types live in .commitlintrc.yml).

Repository prerequisite

In GitHub, enable Settings → Actions → General → Workflow permissions → Allow GitHub Actions to create and approve pull requests. The release-pr job's pull-requests: write permission is necessary but does not override that repository setting. If the setting is disabled, release-plz can push its release branch but PR creation fails with HTTP 403, leaving the branch without a pull request.

Per-release flow (steady state)

During release freeze, default new work to the next milestone. Promote only reproducible correctness, safety, public-contract, or documentation blockers, plus genuinely small mutation-strengthening tests; record why shipping is riskier than changing.

  1. Merge feature/fix PRs to main as usual (Conventional Commits).

  2. Perform one release-wide documentation-freshness sweep over the root and crate READMEs, docs/, examples/, and current version/status claims. Preserve clearly historical references. Stage the intended release line in the dependency snippets and current tool.version examples before dispatch:

    cargo run -p animsmith --example stage_release_docs -- --version <next>
    

    That rewrites every claim the release_version_docs workspace test reads, and nothing else: it edits only the located spans, so a historical version on the same page stays. The test accepts exactly the next patch or minor while main still carries the previous manifest version, and the example refuses any other version. On the generated release-plz-* branch the same test requires every current version claim to equal the bumped workspace manifest. Both deliberately ignore CHANGELOG.md, this completed bootstrap, and roadmap history. The dispatch job checks that generated branch itself in strict mode before succeeding, because GitHub does not initially trigger PR workflows for a PR created with the default GITHUB_TOKEN.

  3. When main is ready to release, manually dispatch the release workflow:

    gh workflow run release-plz.yml --ref main
    

    The workflow first runs the shared checks, then release-plz opens or updates the release PR. It computes the next version from the commits since the last release — one shared version across the publishable crates (version_group), so the whole workspace moves together — and writes the changelog. Ordinary pushes to main never create or update this PR. The release-PR job temporarily installs three exact, commit-scoped Git replacements so release-plz can inspect a short historical branch whose manifests did not parse. The preparation refuses any object-identity drift or pre-existing replacement, preserves commit identities and messages, and is removed even when release-plz fails. It is local to that job: the push-triggered publication path always reads canonical repository history. If another change merges to main while the release PR is open, dispatch the workflow again before merging so the version and changelog include it.

  4. Review that PR. If step 2 staged nothing, or staged a version release-plz did not pick, restate the documentation from the bumped manifest on the release PR branch itself and commit what it writes:

    cargo run -p animsmith --example stage_release_docs
    cargo test -p animsmith --test release_version_docs
    

    With no argument the example targets [workspace.package] version, so on a correctly staged branch it reports no changes. The test is the fail-closed check either way: on a release-plz-* branch it fails unless every current version claim equals the bumped manifest.

    The glTF writer records the package version in asset.generator, so regenerate and commit the version-stamped example assets from the release PR branch before merging:

    cargo run -p animsmith --example gen_example_assets
    cargo test -p animsmith --test examples_cookbook
    

    That refresh moves the stamp and nothing else, so the PAYLOAD_IDENTITIES pins in crates/animsmith/tests/examples_cookbook.rs must come through it untouched: the test above digests each asset around its asset.generator string, and a pin that moves means the regenerated animation changed rather than only its version. Do not edit those literals as part of a release.

    The committed documentation visuals under docs/visuals/ are rendered from those same assets, so regenerate and commit them in the same step:

    cargo run -p animsmith --example gen_docs_visuals
    cargo test -p animsmith --test docs_visuals
    

    The isolated Bevy readback probe locks animsmith-core and animsmith-engine at the workspace version, and the engine states that lock's size and SHA-256 in crates/animsmith-engine/src/bevy_readback_lock.txt. Move both package versions in tools/bevy-readback/Cargo.lock to the release, rewrite that file from the lock, and confirm the lock gate agrees:

    just bevy-readback-lock-refresh
    just bevy-readback-lock
    cargo test -p animsmith-engine
    

    Do not hand-edit the identity file: just bevy-readback-lock renders those two lines again from the committed lock and fails on any difference.

    The cookbook drift guard fails if the committed bytes do not match the release version. The example assets, documentation visuals, and Bevy lock are the three mechanical regenerations this step retains. Payload identities protect the example assets and exact-artifact assembly fixture independently of the package version; the visuals and Bevy lock/identity remain exact regenerated artifacts checked by their own gates. There are no manual assembly digest literals to refresh and no counterfactual assembly rebuild to perform.

    When you merge the PR, the release job runs on the resulting push to main, tags the release, creates the GitHub Release, and publishes every crate to crates.io in dependency order (animsmith-core → -gltf/-fbx/-engine/-report → animsmith). The follow-on release_binaries job calls release-binaries.yml, builds CLI archives from the tag, and uploads the archives plus matching .sha256 files to that GitHub Release. The release_pages job dispatches docs-pages.yml on main in the same run, moving the Pages root to the tag just published; see GitHub Pages documentation.

Supported CLI archive targets live in release-targets.json. scripts/release-targets.py renders the generated workflow matrix block in .github/workflows/release-binaries.yml and the install table in docs/cli.md. After changing release targets, run scripts/release-targets.py write and then just release-packaging so the generated docs and workflow matrix stay in sync.

Post-release verification

Treat workflow success as the start of verification, not the final claim. Before closing the milestone:

  1. Confirm the push-triggered release-plz.yml run completed successfully at the release-PR merge commit, including every platform build and the release asset upload. Confirm the release tag resolves to the frozen, audited release PR head; with merge commits this is normally the release commit, not the merge commit itself.

  2. Verify every publishable package exists at the expected version on crates.io and is not yanked. The crates.io API may return HTTP 403 to an anonymous default client, so use a descriptive curl -A user agent when querying it.

  3. Compare the GitHub Release assets with release-targets.json: expect one archive and one .sha256 file per target. Download them, run sha256sum -c, inspect .tar.gz contents with tar -tzf, and inspect .zip contents with python3 -m zipfile -l so verification does not depend on unzip being installed. Before closing the milestone, run --version from at least one archive that is native to the verification host, confirm the output names the expected release version, and retain that comparison. If no release target is native to that host, perform the same execution, expected-version comparison, and evidence retention on a fresh VM or runner for one supported target. If gh release download leaves the destination empty, fetch each release asset by id through gh api -H 'Accept: application/octet-stream' repos/<owner>/<repo>/releases/assets/<id> instead.

  4. Check docs.rs separately. A crate appearing in the docs.rs queue means it was accepted, not rendered; report it as queued until the versioned pages or build record resolve successfully.

  5. Confirm the Pages deployment followed the release. The release_pages job dispatches docs-pages.yml, so the composing run appears in Actions as Pages documentation triggered by workflow_dispatch. Check the published routing record rather than the run alone:

    curl -fsSL https://mmannerm.github.io/animsmith/BUILD-INFO.txt
    

    It must report Release root: vX.Y.Z for the tag just published and Development subtree: main. If it still names the previous tag, run the manual recovery dispatch from GitHub Pages documentation and re-check.

  6. Re-query the live bug list and release milestone. Close the milestone only when it has zero open issues, then update the roadmap ledger with the shipped date and bounded scope.

crates.io publishing uses Trusted Publishing (GitHub OIDC): the release job holds id-token: write and release-plz mints a short-lived token itself — there is no long-lived CARGO_REGISTRY_TOKEN.

The publish step is idempotent: a re-run skips versions already on the registry. If publishing fails partway through, use Re-run failed jobs on that original push-triggered workflow run. A new manual dispatch creates or updates the next release PR; it does not retry publishing.

Version-bump policy

Configured in release-plz.toml:

  • feat bumps the minor, fix/perf the patch, even on 0.x (features_always_increment_minor = true, porting the old cliff.toml bump rule).
  • Breaking changes follow semver — on 0.x that is a minor bump (0.1.0 → 0.2.0), not a major. This is the one place the old cliff.toml differed: it forced breaking → major (→ 1.0.0), but release-plz has no equivalent setting. If you want to go to 1.0.0, bump the version explicitly in the release PR.
  • Only feat/fix/perf/revert appear in the changelog and release notes; chore/ci/docs/style/refactor/test/build and merge commits are skipped ([changelog].commit_parsers) — they still count toward whether a release is warranted.

Schema identities

Output and measurement schemas use immutable protocol URNs, independent of package releases. Do not rewrite $id during a release. A breaking contract gets a new URN and schema file. scripts/check-schema-id.sh checks that each schema, the CLI, and docs/output.md reference the same identities; repository links are only retrieval locations.

The crate READMEs are included in the crates published to crates.io. During pre-1.0 development, those READMEs intentionally link deeper repository docs to latest main with github.com/mmannerm/animsmith/blob/main/... or /tree/main/... URLs. That means an older published crate can send readers to newer source docs. For now this is accepted so reference docs stay simple while the API is still settling; the machine-readable JSON schema remains protected separately by scripts/check-schema-id.sh.

GitHub Pages documentation

GitHub Pages publishes two deliberately different snapshots through the official artifact deployment workflow in .github/workflows/docs-pages.yml. The site root, https://mmannerm.github.io/animsmith/, is rebuilt from the latest published release tag. https://mmannerm.github.io/animsmith/dev/ is rebuilt from protected main at the same deployment, so contributors can link to unreleased work without relabeling it as released documentation.

The workflow source is the default branch; pull requests only retain a rendered preview artifact and never deploy. The release root is selected from GitHub's latest published release, rather than from an assumed version prefix. Its mdBook executable and its scripts/build-docs-site.py both come from that selected tag, so a new site shape on the default branch never rewrites — or breaks — an already released documentation tree; the /dev/ subtree separately uses the default branch's own pin and build script. If no published release exists, do not enable Pages deployment: publish the first release first, then dispatch the workflow as shown below.

A successful release publication dispatches that workflow itself: the release_pages job in .github/workflows/release-plz.yml runs gh workflow run docs-pages.yml --ref main --repo "${GITHUB_REPOSITORY}" with the repository GITHUB_TOKEN as soon as release-plz reports a published release. That job checks nothing out, so gh cannot infer the repository and the flag is explicit; the recovery command below is the same dispatch run from a clone, where --repo is unnecessary. The dispatch is required, not decorative. GitHub creates no workflow run for an event produced with the repository GITHUB_TOKEN, so the workflow's own release: published trigger stays silent for an automated release — it still covers a release published by hand — while workflow_dispatch is one of the two documented exceptions to that recursion guard. The push that merges the release PR also runs the workflow, but it runs before the release exists and would leave the root on the previous tag; the shared pages-<ref> concurrency group cancels that earlier run when the post-publication dispatch starts.

Recover a missing or stale deployment with the same dispatch from a clone of the repository — after a failed release_pages job, or a release published by hand:

gh workflow run docs-pages.yml --ref main

scripts/check-pages-release-trigger.py, run by scripts/check-github-community-files.sh, fails the build if that dispatch, its actions: write permission, or the workflow_dispatch trigger it targets disappears, or if deployment stops being gated on an eligible published release.

docs/README.md is the canonical task index and the site's only routing authority. Its Category column — Part or Part › Group — generates the mdBook parts, group chapters, and their order during clean staging; do not add a hand-maintained SUMMARY.md, generated HTML, or symlink-based publication tree. docs.rs remains the Rust API reference and must remain prominent in the Pages index and README. Animation-pack reports are nested from the canonical table in docs/reports/README.md, and built README.md chapters receive compatibility routes for mdBook's README.html rewrites. Generated source-reference routes are pinned to the selected release tag at the release root and to main below /dev/.

The site's presentation is tracked rather than configured at deploy time. docs/site/ holds the theme override mdBook applies — stylesheet, fonts, and favicons — staged outside the published source so nothing in it becomes a page, and the current build script refuses a checkout that does not track its stylesheet. Because each release root is built by its own tag's script, release tags predating docs/site/ still publish with mdBook's default theme and their own navigation; do not patch a tag to add either. Retiring or renaming a published route requires its docs/site/redirects.toml entry in the same change, so an existing external link keeps resolving, and a redirect whose target is missing fails the build instead of shipping a dead route. Run just docs-check before changing any Pages source; DEVELOPMENT.md, "GitHub Pages preview", owns the local commands and what they verify.

If a future release needs version-pinned README links, update the READMEs, this section, and scripts/check-package-inventory.sh in the same release-oriented PR. Do not add release-time rewriting without a mechanical check that proves the packaged README links and the release tag agree.

Bootstrapping a newly added crate

Trusted Publishing cannot create a crates.io package. Generate the release PR, freeze its exact head, and run the full release gates before bootstrapping a new workspace crate. Do not change the release PR after any package from that head has been published.

If the new crate verifies against the internal dependency versions already on crates.io, manually publish only the new crate with a token carrying publish-new. If it consumes internal APIs introduced by the same release, first manually publish the required existing workspace crates from the frozen release head in dependency order with publish-update, then publish the new crate with publish-new. Always let Cargo perform its ordinary package verification; --no-verify is not a bootstrap mechanism.

Add the repository's release-plz.yml Trusted Publisher to the new package, then merge the unchanged release PR. release-plz skips the exact versions already published from that head and publishes the remaining workspace crates in dependency order. Record every manual package publish, the exact release-PR commit, and the Trusted Publisher setup in the release audit.

If the release PR must change after a manual publish, stop and reconcile the already public immutable package rather than pretending the replacement head has the same bytes.

One-time bootstrap

This repository starts its public release history from a clean slate: the pre-publication development tags (v0.1.0–v0.7.0, never on crates.io) were deleted on 2026-07-04, so the first crates.io publish, the first GitHub Release, and CHANGELOG.md all begin together at the 0.1.0 in Cargo.toml.

The entire first 0.1.0 release is manual — crates.io publish, the v0.1.0 tag, the GitHub Release, and the initial CHANGELOG.md. Two constraints force this, and they compose:

  • Trusted Publishing cannot publish a crate that does not yet exist, so the first crates.io publish of each crate must use a token.
  • release-plz release only acts on unpublished packages. Once 0.1.0 is on crates.io it will (correctly) no-op — so it will not create the first v0.1.0 tag/Release for you. The manual v0.1.0 tag is also the baseline release-plz needs to compute the next version.

So automation begins at 0.2.0; 0.1.0 is done by hand, once:

  1. cargo login with a token from https://crates.io/settings/tokens (scope: publish-new + publish-update).

  2. Generate and commit the changelog first — before publishing. release-plz update compares the local crates against the registry to find unreleased changes, so it only produces the 0.1.0 changelog while the crates are still unpublished. Run it now (its Keep-a-Changelog format matches every later release):

    release-plz update          # writes CHANGELOG.md
    git add CHANGELOG.md && git commit -m "chore(release): 0.1.0"
    git push
    
  3. Publish the workspace from that release commit, in dependency order (each dependent crate resolves against the crate just published):

    for crate in animsmith-core animsmith-gltf animsmith-fbx animsmith-report animsmith; do
      cargo publish -p "$crate"
    done
    

    animsmith-core should pass cargo publish --dry-run first; the dependent crates can only fully verify once their animsmith-* dependencies exist in the index.

  4. After each crate is accepted, docs.rs queues its documentation. Check each crate's landing page, source listing, and feature metadata. The library crates (animsmith-core, animsmith-gltf, animsmith-fbx, animsmith-engine, and animsmith-report) must also have successful rustdoc builds; their manifests set documentation links and [package.metadata.docs.rs] so pure-Rust crates get Linux/macOS/Windows pages, while the C-dependent animsmith-fbx uses the Linux default target. The published animsmith package is intentionally bin-only: docs.rs has no library target to document and its cargo rustdoc --lib failure is an accepted, mechanically guarded exemption. Verify its docs.rs landing/source/features pages only.

  5. On crates.io, for each publishable crate: Settings → Trusted Publishing → add publisher — repository mmannerm/animsmith, workflow release-plz.yml, no environment.

  6. Tag the release commit and publish the GitHub Release from the 0.1.0 changelog section (release-plz won't create it — 0.1.0 is already published — so the notes are extracted from that same section):

    git tag v0.1.0 && git push origin v0.1.0
    gh release create v0.1.0 --title v0.1.0 \
      --notes-file <(awk '/^## \[0\.1\.0\]/{f=1;next} /^## \[/{f=0} f' CHANGELOG.md)
    

    Then dispatch the binary packaging workflow against main so the manually created release gets the same archives and checksums as later automated releases:

    gh workflow run release-binaries.yml --ref main -f tag=v0.1.0
    
  7. Arm the release automation. Both the manually dispatched release-pr job and the push-triggered release job are gated on vars.RELEASE_PLZ_ARMED, so the whole flow stays inert until this is set — no release PRs and no publish attempts before the manual 0.1.0 above:

    gh variable set RELEASE_PLZ_ARMED --body true
    

After the bootstrap, every subsequent release (0.2.0+) starts with the manual workflow dispatch and then goes through the release-plz PR flow above — no manual cargo publish, no manual version edits, one repo-level vX.Y.Z tag and Release per version.

Known caveat: CI on the release PR

PRs opened with the default GITHUB_TOKEN do not trigger on: pull_request workflows, so the release-plz PR will not get its own full shared-check matrix of its own. The manual dispatch runs that matrix against pre-bump main, then checks out the exact generated branch and runs the focused release-version documentation guard in strict mode. The generated lockfile, changelog, and assets still require review, and the post-merge checks job runs the full matrix again before publishing. If branch protection also requires a full-matrix check attached to the release PR itself, give the release-pr job a PAT or GitHub App token via the release-plz token input instead of secrets.GITHUB_TOKEN.

Support

animsmith uses GitHub issues for project support. GitHub Discussions are not enabled for this repository.

Questions

For usage questions, open a question issue:

https://github.com/mmannerm/animsmith/issues/new

Include the command you ran, the asset format, the animsmith version, and what you expected to happen.

Bugs

Use the bug report template:

https://github.com/mmannerm/animsmith/issues/new?template=bug_report.yml

Please include a minimal public or synthetic asset when possible. Do not attach licensed assets unless you have permission to share them.

Documentation And Examples

Use the documentation-gap template for missing, stale, or confusing docs:

https://github.com/mmannerm/animsmith/issues/new?template=documentation_gap.yml

Example and tutorial gaps belong there too; the issue should say what asset type, command, or embedding workflow the example needs to cover.

Feature Requests

Use the feature request template:

https://github.com/mmannerm/animsmith/issues/new?template=feature_request.yml

Describe the game-animation workflow you want to support and whether it belongs in the CLI, a Rust crate, the report, or documentation.

Security

Do not report vulnerabilities in public issues. Use the private reporting path in SECURITY.md.

Security Policy

animsmith is a pre-1.0 Rust CLI and library workspace. Security fixes target the current development line and the latest published crates once the project is on crates.io.

VersionSupported
current mainyes
latest crates.io releaseyes, after first publish
older pre-1.0 releasesno, unless a maintainer explicitly says otherwise

Reporting A Vulnerability

Please report suspected vulnerabilities through GitHub private vulnerability reporting:

https://github.com/mmannerm/animsmith/security/advisories/new

Do not open a public issue for security-sensitive reports. Include:

  • Affected crate or CLI command.
  • A minimal reproducer or input description.
  • Expected impact.
  • Whether the report depends on untrusted animation input, build-time behavior, or generated HTML reports.

Maintainers will triage the report, discuss remediation privately, and publish an advisory if the issue has user impact.

Scope

Security-sensitive areas include parser hardening for glTF/GLB and FBX input, generated report output, dependency advisories, and behavior that could crash or hang CI on untrusted assets.

Correctness bugs, false positives, and missing animation checks should be filed as normal GitHub issues unless they expose a security impact.

animsmith — design & requirements

Status: pre-1.0 publishing design. Intended to keep the public crate and CLI surface aligned while the project is still willing to make breaking changes. Origin: extracted from a private game project's animation pipeline (design session 2026-07-03); that project — "the incubating project" below — is the first consumer, not the scope.


1. Mission & positioning

animsmith is a linter for skeletal animation clips. It answers the question every game team answers by hand today: does this clip have game-engine-friendly characteristics? — does the loop actually close, does the walk cycle's declared speed match its root motion, do the feet slide during stance, is the rig conformant, are the quaternions sane.

The gap it fills. Nothing open-source does game-semantics clip validation:

  • Khronos glTF-Validator checks spec conformance (accessor validity, NaN, quaternion norms at the container level) — it has no concept of a loop, a gait, or root motion.
  • ozz-animation has a motion-extraction sample (a good root-motion measurement reference) but no lint pipeline.
  • Academic metric code (foot-skate ratio, jitter, penetration) lives in ML-evaluation repos, not artist tools.
  • Engine importers (Unreal Interchange + Data Validation, Unity AssetPostprocessor, Godot import sidecars) give teams a place to hang custom checks, but the checks themselves are always studio-custom and re-derived from scratch.

animsmith packages those checks as a standalone Rust library + CLI: glTF/GLB native, FBX ingested via ufbx, engine-agnostic core, machine-readable output, and a self-contained HTML report with a 3D preview.

What it is not (scope guardrails):

  • Not a spec validator. Run glTF-Validator for container conformance; animsmith assumes a parseable file and judges its content.
  • Not an art exporter. convert (FBX→glTF) exists so clips can enter the lint pipeline directly from a DCC export; it promises animation and skinning fidelity, not material/shading fidelity.
  • A transformer for pipeline-mechanical operations only (scope widened 2026-07-03; see Appendix A). In scope: fix for lossless mechanical repairs (quaternion unit normalization and hemisphere normalization), frame-range slice/trim + hold-extend, fail-closed in-place gait-anchor rotation, opt-in pruning of provably constant multi-key tracks, and format conversion including a full mesh/skin FBX→glTF path (a maintained replacement for the archived FBX2glTF). Out of scope stays artistic transformation: retargeting, motion editing, procedural animation — that is DCC work. The rule of thumb: animsmith may rewrite a clip only in ways whose correctness its own checks can verify.
  • Gait anchoring is an explicitly in-place operation. The core boundary requires a movement policy; the shipped CLI and assembly switches select only InPlace. Before any channel is cyclically reordered, AnimSmith samples the resolved Root role (falling back to Hips) and refuses missing/non-finite evidence, horizontal accumulation above 1 cm, or yaw accumulation above 1°. Every nonconstant channel the operation would rotate must contain exactly one key at each declared whole-frame sample over [0, duration], at the exact representable f32 key / fps time and exact period endpoint. Sparse, differently framed, duplicate-time, or off-grid evidence refuses; duplicate (bone, property) channels, including constant channels, also refuse before sampling. A phase shift must bijectively permute authored values rather than synthesize values at omitted frames. The verifier samples those exact admitted f32 key times, and mutation is an integer-index permutation rather than a second floating-point resample. Constant channels need no grid because cyclic reordering cannot change them; their key times cannot influence the declared period or shift. Before allocating a pose grid, the public core boundary validates all track cardinalities, targets, finite values, all resolved metric-role indices, and the complete acyclic parents-before-children skeleton. Both declared-frame and maximum-authored-key work must independently satisfy the inclusive bounds declared frames × skeleton bones <= 1,000,000, declared frames × tracks <= 1,000,000, and maximum authored keys × skeleton bones <= 1,000,000; every verifier and metric sample uses the one bounded declared grid. The 1 cm endpoint-displacement and 1° accumulated-yaw caps are applied directly, without any sampled-step allowance an interior outlier could inflate. Yaw is derived from model-space f32 quaternions as binary64 headings. At sample zero, the verifier selects the local +Z, +Y, or +X basis axis with the greatest finite horizontal projection, breaking exact ties in that order, and retains that one axis for the entire proof. This admits equivalent source-axis conventions without switching witnesses later to hide yaw; a selected axis that later loses its horizontal projection refuses. Full-turn crossings are counted and the first heading is subtracted from the final unwrapped heading, so the result has no segment-count-dependent summation error. Only four f32 successors at each inclusive cap cover authored endpoint translation/quaternion quantization; this gait-local rule does not widen other checks. Standalone transform output is transactional too: per-clip success lines and the write summary remain buffered until every selected clip and the artifact write succeed. This admits tightly closed cyclic pelvis/root sway while refusing authored travel and turns. Root-motion phase offsets, root-motion extraction, and trajectory-preserving cyclic rebasing remain runtime or separately designed operations; gait anchoring must not improvise them.
  • Not a runtime. It models how engines sample animation; it does not play games.

2. Users & use cases

  1. Artist inner loop — animsmith lint export.fbx seconds after a DCC export, before any engine import or bake. Catches "the loop pops", "wrong rig", "cm instead of m" while the DCC session is still open. This is the highest-value loop: the alternative is discovering the problem after the slowest step of the pipeline.
  2. CI gate — animsmith lint --format json in CI on committed assets; stable JSON schema, exit codes, per-check severity config, baseline file for adopting teams with a dirty back catalog.
  3. Pipeline library — engine pipelines embed animsmith-core and build check sets programmatically. First consumer: the incubating project's asset gate replaces ~1000 LOC of measurement Python with library calls.
  4. PR-review artifact — animsmith report clip.glb -o report.html produces a single offline HTML file with 3D skeleton playback and metric charts; attach it to a PR or CI artifacts so a reviewer can see the seam pop the numbers describe.

3. CLI surface

animsmith lint    <file...> [--config animsmith.toml] [--select ids] [--deny warn] [--format text|json]
animsmith measure <file...> --format json          # measurements only, no judgment
animsmith inspect <file>                           # clips, durations, tracks, bones, detected rig profile
animsmith report  <file> -o report.html [--clip name]
animsmith report  <before> -o comparison.html --compare-after <after> --before-clip <name> --after-clip <name>
animsmith transform <file> -o <out.glb> [--clip name] [--slice START:END] [--hold-extend SECONDS] [--gait-anchor] [--drop-duplicate-loop-endpoint] [--prune-constant-tracks]
animsmith fix     <file> (-o <out.glb>|--in-place|--dry-run) [--repair id[,id]]
animsmith convert <in.fbx|in.glb|in.gltf> -o <out.glb> [--material-texture-recipe recipe.toml] [--animation-only|--bake-static-mesh-transforms] [--format text|json]
animsmith assemble <recipe.toml> -o <out.glb> --evidence <out.json>
animsmith diff    <A> <B> [--format text|json]     # A/B: assets or one-file output-v13 measure/lint JSON
  • lint = measure + judge against config. measure is lint minus judgment — both emit the independently versioned measurement contract that other pipelines can pin.

  • Exit codes: 0 no failing findings and no required-unavailable engine prediction facets (warnings, notes, and ordinary nonblocking coverage gaps may remain), 1 at least one error-severity finding, any required_prediction_unavailable facet, pending repairs under fix --dry-run, or any producer refusal established as a fact about source asset bytes, 2 operator/tool error (unreadable declared file, bad config or recipe syntax, unsafe/unwritable path, or publication failure). --deny-warnings promotes warnings to errors; severity and --allow do not suppress an emitted required-unavailable facet.

  • Producer outcome authority is typed before prose exists. convert and assemble carry Published, Rejected, and Operator through one shared boundary. Loader, selector, transform, proof, and representability failures reached after source bytes were read are asset refusals; invocation, recipe-validation, path, file-read, temporary-file, serialization, and publication failures are operator errors. Mixed error enums (notably the material-texture recipe) are matched by variant at the call site. No exit decision parses an error string. JSON refusals use the separately immutable urn:animsmith:schema:producer-refusal:1 record; existing conversion, assembly, output, and scale evidence identities remain unchanged. Text refusals are one escaped stderr diagnostic carrying the same stable kind. Every refusal precedes publication, preserving a prior assembly pair and a prior convert artifact; this does not claim that convert's direct writer can roll back an operator I/O failure.

  • Stdout is reporting, not outcome authority. Every JSON, text, and Markdown stdout write, including parser-rendered help and version text, uses a fallible checked write followed by a best-effort checked stderr diagnosis. A closed pipe or other write failure never panics or replaces the outcome already established by a stdout-bearing path: shipped success paths retain 0, while finding and refusal paths retain 1. Operator errors occur before stdout reporting, remain stderr-only, and retain 2. This deliberately includes output-centric commands such as inspect: exit 0 states that inspection completed, not that an external consumer accepted every byte. Assigning a new outcome to text alone would make exit semantics depend on presentation format and would relabel real findings or completed publication as operator errors. JSON serialization failure remains an operator error because it means no truthful record could be formed; failure to deliver already-rendered bytes is only a reporting failure. Multi-part human output is attempted as one checked stream where one command can produce multiple records; in particular, all selected fix repair reports and all parts of one conversion summary yield at most one stdout-failure diagnostic. Parser output remains clap-owned: the checked boundary calls clap's fallible writer directly so its configured automatic/forced ANSI styling is preserved instead of being stripped while materializing a plain string.

  • Human-readable command results are assembled by pure functions in the CLI's renderer module. Command dispatch keeps execution, file writes, and exit policy in main.rs and passes structured values to the renderer; the renderer is the single owner of escaping untrusted text for terminal-safe presentation. JSON result serialization remains a separate, unchanged path.

  • transform --prune-constant-tracks is an opt-in, candidate-first mechanical edit: it shares the constant-track tolerances (vector components 1e-4, sign-invariant rotations 1e-3 radians), verifies the resulting sampled local TRS and model-space position/rotation, and reports every original track index it removed or retained. It runs after selected transforms and never substitutes for DCC curve cleanup or key reduction. Per-clip animates_bones names protect motion evidence; [rig] required_bones remains a skeleton-presence-only contract.

  • Inputs: .glb, .gltf (+ external buffers), .fbx (via the fbx feature, default-on in the released binary).

  • Malformation policy: structural malformation — keyframe/value count mismatch, zero-key channels, absolute or escaping external buffer URIs, non-forest node graphs (cycles or a node with two parents), or a primitive accessor whose dense or sparse byte extent its declared buffer view or resolved buffer bytes cannot satisfy, an integer TEXCOORD_0 or WEIGHTS_0 accessor that omits normalized: true, or an animation sampler accessor whose declared element does not match its property-selected reader — is rejected at load. For convert and assemble, a rejection derived from readable source bytes is an asset refusal (exit 1); an unreadable primary file or a missing/unreadable declared external resource is still an operator error (exit 2). Commands outside that producer boundary retain their historical operator-error load policy. Run glTF-Validator for structural details. Recovering a non-forest graph would force an arbitrary parent choice or silently drop a cyclic subtree, so the loader rejects rather than repairs (decision recorded for #92). Likewise, substituting an empty position/index vector for a short accessor would make checks report on geometry the file did not author. Primitive shortfalls therefore use a located PrimitiveAccessorLayout refusal; inverse-bind shortfalls remain typed source evidence because that domain already models unavailable bind declarations (decision recorded for #329). Sampler input is SCALAR/FLOAT; translation and scale output are VEC3/FLOAT; rotation output retains all five glTF quaternion encodings (BYTE, UNSIGNED_BYTE, SHORT, UNSIGNED_SHORT, FLOAT) as VEC4, and morph-weight output retains those same component encodings as SCALAR. Declared lookalikes are never reinterpreted merely because their byte size happens to match the reader's element (decision recorded for #327). The same fail-closed rule keeps invariant-9 honest for integer UVs and weights: normalized UNSIGNED_BYTE/UNSIGNED_SHORT values are decoded to their declared float range, and FLOAT values pass through, but an unnormalized integer accessor is refused before decoding. The gltf reader rescales those integers whether or not the flag is set, and the loader has no finding channel in which to disclose that unauthorized reinterpretation; allowing it would make measure report derived values as authored data (decision recorded for #328). Semantic defects — NaN times or values, non-unit quaternions, hemisphere flips, seam pops — load fine and are judged by the checks; sampling is panic-free under them by construction.

  • fix intentionally requires either -o/--output or --in-place for writes; --dry-run is the check mode — it inspects only and exits 1 when repairs are pending, mirroring lint. Repairs are addressed by stable ids; every repair must be safe, lossless, and idempotent. Repair taxonomy (risk-tier groups) is deliberately deferred until a repair exists that doesn't meet that bar.

  • fix stays byte-surgical — a product requirement, not an accident (decision recorded for #33). It patches only the offending animation bytes in the original container and copies everything else through verbatim, so meshes, skins, materials, and textures survive a repair bit-for-bit. Folding hemisphere/norm repair into a core transform and re-emitting through the unified Document writer was considered and rejected: the model writer re-emits and reorders accessors, so it is not byte-identical and would rewrite bytes fix must leave untouched. The Document round-trip is the right tool for convert/transform; in-place fix is not a round-trip.

  • convert and assemble are compiled only with the fbx feature. --no-default-features remains a glTF-only pure-Rust CLI with validation, transform, fix, and diff commands intact; report is controlled separately by the report feature.

  • convert --bake-static-mesh-transforms is an opt-in static geometry operation. It accumulates rest hierarchy transforms into positions and inverse-transpose normalized normals, then writes canonical identity-root geometry. It retains topology, UVs, and the model-supported material and embedded base-color, normal, metallic-roughness, and occlusion-texture data. It fails closed for any animation track, skin signal, uninstanced or shared mesh definition, malformed/non-finite data, singular or near-singular transform, or reflection; it neither bakes skinning nor guesses animated or reflected semantics. It conflicts with --animation-only, leaves default conversion unchanged, and is deterministic for repeated same-platform input/options.

  • convert --material-texture-recipe supplies exact named BaseColor, normal, metallic-roughness, and occlusion mappings for a conversion. It conflicts with --animation-only, is compatible with static baking, and leaves the ordinary linked/embedded texture path untouched when omitted. Its versioned recipe and provenance contracts pin path containment, image processing, and encoder behavior.

  • assemble is the versioned generic boundary for a skinned base plus clips spread across separate files or master timelines. It permits only explicit, mechanically verifiable operations: exact-name skeleton remap, exact mesh selection, clip slicing/endpoint/hold/gait operations, named channel removal, rest-track completion, quaternion cleanup, opt-in constant-track pruning after all other transforms while protecting effective-clip animates_bones names, fail-closed exact-name node-subtree removal after animation transforms, bind-consistent skin canonicalization, material recipes, and deterministic GLB/evidence emission. Node-subtree removal refuses any surviving track, mesh-instance, skin-joint, or complete source-skin reference; it remaps the surviving hierarchy but does not reparent descendants or garbage-collect mesh, material, or texture resources. Character-assembly recipe/evidence v3 introduced that node-removal contract; v4 retains it unchanged while adding optional rest/bind scale integration. Selectors resolve by exact unique name against the post-canonicalization base; their descendant union is planned before completion, excluded from completion targets, and projected after clip processing and constant-track pruning. animates_bones may therefore retain a protected track and force refusal, while [rig] required_bones remains lint presence policy rather than an assembly selector. Accepted non-empty projection clears optional source-native skeleton identity, because it cannot remain a complete account after authored deletion. Because mesh-bearing nodes are refused, removal cannot newly orphan asset resources; only the earlier explicit mesh-instance selection performs resource pruning. Evidence lists every removed node once in original parent-before-child order, distinguishes direct selectors from descendants, and retains original node and parent indices. Archive extraction, gameplay naming and acceptance policy, cache/generation policy, and publication remain with the consumer.

4. Repository & crate layout

One public repo, one cargo workspace, five published crates (plus one publish = false dev crate, animsmith-testkit). The split is driven by two hard constraints: the core must be consumable with zero C compilation and minimal deps; FBX support pulls in a C build step most library consumers must never pay for.

animsmith/
├── Cargo.toml                  # workspace, edition 2024
├── LICENSE-MIT / LICENSE-APACHE
├── THIRD-PARTY.md              # ufbx (MIT OR PDDL-1.0), vendored viewer assets
├── crates/
│   ├── animsmith-core/          # data model, sampling/FK, metrics, diffs, checks, config, findings
│   ├── animsmith-gltf/          # glTF/GLB → core model; GLB writer for `convert`
│   ├── animsmith-fbx/           # ufbx wrapper → core model; isolates the C build
│   ├── animsmith-report/        # self-contained HTML report generation
│   ├── animsmith/               # CLI binary (features: fbx, report — default on)
│   └── animsmith-testkit/       # publish=false: fixture builders, documentation-source readers, and CLI process helpers shared by tests + the example generators
├── assets/viewer/              # viewer JS/CSS, inlined via include_str!
├── fuzz/                       # cargo-fuzz targets for the untrusted-input loaders
└── testdata/                   # CC0 rigs + procedurally corrupted fixtures
  • animsmith-core: deps glam (the de-facto Rust game-math crate — do not hand-roll mat4/quat as the Python did), serde, serde_json, serde_jcs for strict bounded core-owned RFC 8785 canonical JSON contracts, sha2, and thiserror. Its JCS use remains engine-, format-, filesystem-, and network-agnostic. It owns strict versioned JSON contract validation but no file-format knowledge or filesystem/network I/O. This is what embedding pipelines link.
  • animsmith-gltf: the gltf crate with trimmed features (no image decoding); owns GLB emission via gltf-json.
  • animsmith-fbx: ufbx (official bindings, v0.11.x, actively maintained; bundles the single-file C library via cc — no system deps). A separate crate rather than a feature flag so the C toolchain requirement is structurally isolated.
  • animsmith (CLI): clap, serde_json, toml. --no-default-features yields a pure-Rust glTF-only build.
  • Toolchain: stable Rust, edition 2024, MSRV pinned in CI. License: MIT OR Apache-2.0. All crate names verified free on crates.io (2026-07-03).
  • fuzz/: a nightly-only cargo-fuzz workspace (detached from the main workspace) with libFuzzer targets for the three entry points that ingest untrusted files — animsmith_gltf::load, FixSession::apply_to_path(..., Repair::QuatFlip), and animsmith_fbx::load. These are the executable check on invariant-1 ("untrusted input must never panic or OOM"): targets run in release mode to match the shipped CLI's panic semantics, with AddressSanitizer on. A weekly CI job (fuzz.yml) runs each target for 60s off a checked-in seed corpus; minimized crashers are committed under fuzz/seeds/ as regression fixtures, each also pinned by a unit test in animsmith-gltf's hardening suite. Continuous/long-running fuzzing (OSS-Fuzz) is deferred.

5. Core data model

Two representations of a loaded file, because checks genuinely need both:

Raw layer — what the file says. The document keeps the skeleton hierarchy and rest pose, authored clips and tracks, optional scene assets, and source metadata together. Mechanical checks use this layer for defects such as NaNs, quaternion flips, key density, and constant tracks. Exact Rust types and fields belong to the animsmith-core model rustdocs; build them with just doc or use the package README's stable docs.rs link.

assets (meshes, skins, PBR materials, and embedded base-color, normal, metallic-roughness, and occlusion textures) is the scene-asset half of the document. Both the FBX and glTF loaders populate it from a single load (there is no separate assets-carrying entry point — the two loaders share one shape); it is empty when the input carries no scene assets. The check catalog ignores it — checks judge animation — but it rides the one load/write round-trip, so transform and convert preserve geometry rather than silently dropping it, and measure reports versioned static scene measurements from it (#16): geometry-definition boxes in primitive coordinates, node-instance boxes in default/rest node-world coordinates, and per-scene unions. These are not runtime-visible bounds: animation, skin deformation, morph deformation, and runtime placement are excluded. The output contract records unavailable static node bounds and scene partial coverage rather than serializing non-finite values.

The glTF loader also retains metadata-only presence for secondary JOINTS_n/WEIGHTS_n attributes. The measurement contract aggregates those independent sides per mesh definition so unsupported or unpaired sets are observable without changing the primary four-influence semantics. Secondary per-vertex payloads, repair, and writer preservation remain outside the core skinning model.

The scene-asset model keeps source mesh definitions separate from their mesh-bearing node instances and retains declared scene roots plus the optional default-scene index. That identity is measurement evidence: duplicate names do not collapse, shared definitions are reported once, and nodes outside every declared scene remain observable. The current glTF writer still normalizes an emitted document to its existing single generated scene; preserving authored multi-scene membership across transform/convert is not part of the static measurement contract.

validate_document_shape is the model-owned snapshot check for the structural invariants full-shape strict operations such as scale share: finite parent-before-child rest topology, source identity/projection topology, clip/track storage, and scene instance/inverse-bind references. Document remains publicly mutable, so a successful check is not a durable token; strict operations revalidate every input at every public boundary. This is deliberately not universal model validity. Measurement can retain finite instance evidence from hierarchy branches unrelated to a malformed chain, sampling remains panic-free and tolerant of hostile standalone tracks, and static baking validates only the fields its static operation consumes. Scale's negative-weight, finite-position, capability, closure, affine, parity, and proof rules remain operation-specific.

Ingestion is triangle-list only — the target inputs are skinned game rigs, and the model and writer carry no primitive-topology field. A non-TRIANGLES glTF primitive (points, lines, strips, fans) is skipped rather than misread as a triangle list; other topologies and their retriangulation are out of scope for now.

Sampled layer — what a game runtime sees. A PoseGrid built by a ClipSampler: uniform time grid over [0, duration] (resolution = max channel key count, or explicit fps), glTF-spec interpolation semantics (lerp for T/S, shortest-path slerp for R with negation on dot < 0, STEP hold, cubic-spline Hermite), clamp at ends. For clips declared loop, the wrap pair is (last frame, frame 0) — the seam definition every loop check shares. FK accumulates every skeleton node's local TRS to model space, including each root node's own transform. Metrics that need a body-relative frame derive it from resolved roles such as hips and feet. The metric grid is computed once per clip and shared across checks, measurements, and reports through the lazy MetricGrids owner.

Rig profiles — checks reference semantic roles rather than bone names. Profiles bind those roles to source-rig names; the exact public role set, binding types, and matcher fallback belong to the animsmith-core profile rustdocs.

Built-in profiles ship for mixamo (mixamorig:Hips…), ue-mannequin (pelvis, foot_l…), and humanoid (humanoid_ Pelvis, humanoid_ L Foot…), plus auto-detection that scores every profile by resolved-role coverage and reports the winner in inspect. A check whose required roles don't resolve records a typed coverage gap — never a false finding. This is the single design rule that makes the tool useful outside its birthplace: tolerance data and bone names are config; the math is not.

The runner owns selection, activation, and severity policy. Each check exposes a cheap applicability predicate and one evaluation method. The evaluation returns content findings separately from completed scopes and typed coverage gaps. Severity overrides therefore cannot turn missing evidence into a false error. gait-group, for example, can complete member-existence validation while reporting phase coherence as a gap. severity = "off" disables a check without hiding its applicability record. A check may also declare an opt-in default; assigning "note", "warn", or "error" enables it while retaining the same independent configuration record.

The built-in evidence-code declarations are the single authority for each scope or gap code's machine identity, meaning, and allowed emitting check ids. The evaluation boundary rejects a built-in code emitted by an undeclared check, and the output-reference gate derives its exact inventory, meanings, and emitters from those same declarations. Embedded checks remain open-ended: they use namespaced custom codes rather than extending a closed enum.

Checks implement one trait and emit structured findings:

#![allow(unused)]
fn main() {
pub trait Check {
    fn id(&self) -> &'static str;              // "loop-seam", "quat-flip", …
    fn enabled_by_default(&self) -> bool;       // true unless policy is opt-in
    fn applicability(&self, ctx: &CheckCtx) -> Applicability;
    fn evaluate(&self, ctx: &CheckCtx) -> CheckOutput;
}
pub struct Finding {
    pub check_id: &'static str, pub severity: Severity,   // Note | Warning | Error
    pub clip: Option<String>, pub bone: Option<String>, pub time_s: Option<f32>,
    pub measured: Option<Value>, pub expected: Option<Value>,
    pub message: String,
}
}

The structured fields (not just a message string) are what make diff, the JSON schema, and the HTML report cheap.

6. Check catalog

Tiers are shipping priority. "Prior art" = a proven implementation exists in the incubating project's pipeline to port, with real-world numbers to golden-test against.

P0 — mechanical + the two killer semantic checks (v0 core)

idwhat it checksneedsconfigprior art
nanNaN/Inf in key times or valuesraw—new (trivial)
time-monotonicnon-increasing/duplicate key times; first key ≫ 0rawepsilonnew
quat-normrotation keys with |q|−1 beyond toleranceraweps (1e-3)new
quat-flipadjacent keys with dot < 0 (long-way slerp in engines that don't neighborhood-correct)rawseveritynew
duration-sanityzero/degenerate duration; channels within one clip ending at different times; frame count non-integral at declared fpsraw + metaexpected fps list, pinned durationreference contract duration_s pin
scale-keysinterpolation-aware temporal scale variation beyond toleranceraw trajectoryseveritynew
non-uniform-scalecomponent inequality anywhere on the interpolation-aware scale trajectoryraw trajectoryseveritynew
constant-nonunit-scaleconstant non-unit scale channel or single-key pinraw trajectoryopt-in severity (off by default)new
constant-trackredundant multi-key representation whose interpolation-aware trajectory never movesraw trajectoryepsnew
frozen-bonerequired bone's max angular deviation from first frame below floorgrid + roles/metamin_rotation_degreference contract rotation floor + measured rotation ranges
duplicate-loop-endpointstrict authored-key duplicate closing endpoint in a declared loop; warning/default-oncommon finite strictly increasing timeline, valid cardinality, matching endpoints, interior motion + loop declaration—mechanical subset of #22's endpoint classifier plus open-cycle transform
loop-closuremaximum per-bone model-space last→first position and shortest-path rotation deltagrid + loop declarationglobal max_position_delta_m / max_rotation_delta_deg; clip max_loop_position_delta_m / max_loop_rotation_delta_degconsumer-neutral authoring-loop requirement
loop-seamlast→first position wrap discontinuity of feet-relative-to-hips, normalized by the local neighbour per-frame step, with a stride floor so stationary clips skipgrid + Hips/feet/toe rolesmax_ratio, min_stride_step_mlocomotion_metrics.py — port verbatim
loop-seam-velmaximum per-bone model-space difference between the in-clip velocities entering and leaving the wrapgrid + loop declarationglobal max_velocity_delta_mps; clip max_loop_velocity_delta_mpsconsumer-neutral authoring-loop requirement
loop-seam-rotmaximum per-bone shortest-path model-space angular-velocity difference between the in-clip steps entering and leaving the wrapgrid + loop declarationmax_angular_velocity_delta_degpsconsumer-neutral authoring-loop requirement
root-motion-speedhorizontal root/hips displacement ÷ duration vs declared speed_mps; flags stray speed pins on non-locomotion clipsgrid + Root/Hipspinned speed + tolerance (reference gate: 15%)reference bake
missing-bonesdeclared-required animated bones absent; tracks targeting nodes outside the skeletonraw + metabone/role listreference contract animates_bones
required-bonesdeclared required rig bones absent from the skeleton; static sockets and IK targets are validskeleton + rig config[rig] required_bonesconsumer-neutral structural rig contract
namingclip names vs convention patternmetaregex/globnew
units-sanityhips rest height wildly outside human scale (the cm-vs-m export classic)skeleton + profileheight bandnew

P1 — locomotion semantics (the first-of-kind tier)

idwhat it checksprior art
gait-phase / gait-groupstride-phase anchor from the fundamental-harmonic trough of the left-minus-right foot-height signal; an exactly zero-amplitude signal has no phase subject, while positive signals are governed by the configured inclusive lr_amplitude confidence floor; circular phase spread across a declared clip ring (directional-blend coherence)reference metrics module + gait-group contract — port verbatim
in-placeclassify in-place vs root-motion (net + per-frame root displacement) and compare against the clip's declared expectationnew; trivial on the grid
foot-slidedetect stance (foot height + near-zero vertical velocity), measure horizontal foot velocity during stance in the travel-cancelled framenew; hardest check — ships opt-in until corpus-tuned
bind-poserest pose vs first frame delta (clip authored against wrong bind); T-pose/A-pose classification; node-TRS rest disagreeing with IBM-derived rest (the disagreement is itself a finding)reference sidecar already derives rest from IBMs
axis-conventionscharacter forward/up at rest vs declared axes; root orientation drift over a loopreference contract axis vocabulary
key-densitykeys/sec far above the clip fps (unbaked-curve bloat) or far below (starved track)new

P2 — corpus/cross-clip

Cross-clip skeleton & rest-pose consistency across a directory; ground-penetration of feet/toes; mirrored-pair symmetry (walk_left vs walk_right); additive-clip suitability; compression-noise metrics (per-track jerk); morph-target weight ranges; SARIF output.

7. Configuration

TOML (animsmith.toml, or --config); Rust-ecosystem norm and diff-friendly in asset repos:

[rig]
profile = "mixamo"                 # or "auto", or an inline role map:
# A presence-only structural contract; these do not need animation tracks.
required_bones = ["root", "weapon_socket", "ik_hand_l"]
# [rig.roles]
# hips = "humanoid_ Pelvis"
# left_foot = "humanoid_ L Foot"

[checks.loop-seam]
severity = "error"                 # off | note | warn | error
max_ratio = 1.5

[checks.loop-closure]
max_position_delta_m = 0.01
max_rotation_delta_deg = 1.0

[checks.loop-seam-vel]
max_velocity_delta_mps = 0.1

[checks.loop-seam-rot]
max_angular_velocity_delta_degps = 5.0

[checks.quat-flip]
severity = "warn"

[clips."run_*"]                    # glob; exact > glob, later entries win ties
loop = true
movement_owner_xz = "animation"
movement_owner_y = "gameplay"
movement_owner_yaw = "animation"
max_loop_position_delta_m = 0.04   # finite per-family override; global is fallback
max_loop_rotation_delta_deg = 2.0
max_loop_velocity_delta_mps = 0.2
speed_mps = { value = 3.1, tolerance = 0.25 }
fps = 30

[clips.run_forward]                # exact fields overlay matching globs
max_loop_rotation_delta_deg = 0.5

[gait_groups.run-ring]
clips = ["run_forward", "run_back", "run_left", "run_right"]
max_gait_phase_spread = 0.08
min_lr_amplitude_m = 0.05

CLI flags override file config (--select, --allow, --deny).

Engine-agnosticism rule: the TOML file is merely one constructor of a CheckSet. Embedding pipelines build check sets programmatically through the library API and keep their own contract formats, hashing, and tolerance semantics on their side. animsmith never learns an embedder's contract schema.

8. Output formats

  • Text (default): findings grouped per clip, measured-vs-expected on one line, colored; --quiet for CI summaries.
  • JSON (--format json): current output v19, identified by urn:animsmith:schema:output:19. Lint emits one result per catalog check and represents selection, configuration, applicability, evaluation coverage, content findings, completed scopes, and typed gaps independently. Measure and lint share a nested, independently versioned measurement contract. The current contract inventories glTF material definitions, the five core base_color, normal, metallic_roughness, occlusion, and emissive texture slots, texture-to-image identity, and decoded image metadata in source order. Complete coverage is scoped to that documented core domain; extension-defined texture slots are not implied. Measurements-v17 makes per-bone loop-continuity unavailability explicit without suppressing finite sibling rows. It retains measurements-v16's inventories each mesh's primitives in source order: nullable source material, total and finite decoded POSITION rows, and finite-only primitive AABB and centroid. Indexed geometry counts each stored position once rather than index references. Unsupported nonempty images may carry at most 16 bytes of lowercase leading_magic_hex; it is evidence only and never a guessed container format. The contract makes absent loader support explicit as unavailable coverage and separates a source-declared MIME type from byte-detected container and decoded image facts. This remains measurement evidence, not an image acceptance, repair, resize, transcode, color-space, writer-preservation, or recipe-authority policy. Measurements v15 adds the canonical set of surviving non-empty local TRS channels keyed by skeleton index, plus engine-neutral sampled Root/Hips trajectory evidence. Root is selected whenever resolved; Hips is only a fallback when Root is unresolved. Translation and yaw retain independent availability, with endpoint X/Z/Y displacement, sampled horizontal travel and vertical extrema, and fixed-basis net/unwrapped/travel yaw in normalized right-handed +Y-up metre space. These are shared uniform-grid regression observations, not continuous-curve extrema or transform-correctness proof. Output v10 added the registry-independent engine-prediction provenance and scoped-facet substrate described in Appendix E. Output v11 retained it and added per-role resolution outcome and match-policy provenance. Output v12 introduced bounded V2 provenance/settings/prediction evidence and catalog-allocated facet summaries; output v17 retained that evidence while advancing its nested measurements to v16 and adding V6 root-motion provenance. Output v19 retains the immutable prediction graphs against a fail-closed measurements-v16 projection while publishing the complete measurements-v18 evidence. Output-v9, measurements-v15, output-v12, and collection-output-v4 remain immutable historical contracts. Measurements v14 gives every clip fact that is not applicable to every clip (loop continuity, loop endpoint mode, frame grid, loop seam ratio, gait and its phase, and root-motion speed) a required sibling <field>_availability status of measured, not_applicable, or unavailable, so a consumer can distinguish a legitimately missing subject from an applicable metric whose derivation failed and must fail closed. It also retains v13's exact raw inverse-bind matrices and affine and conditioning evidence before deriving their inverses, and v12's corrected linear-transform observations through shared f64 affine facts; v13 and earlier contracts remain immutable historical evidence. convert --format json instead emits the separately versioned urn:animsmith:schema:conversion-evidence:2 producer-evidence contract: requested options, written-artifact counts, optional static mesh bake entries with source/output identities and applied world transforms, and deterministic material-texture recipe provenance when requested. v1 remains historical; the current CLI emits v2 exclusively. CLI exit status derives from content severity (warnings block only with --deny-warnings) plus required-unavailable engine-prediction facets; ordinary coverage gaps remain nonblocking evidence. The current output-v19 envelope types and immutable identities live in animsmith-core so CLI and embedded producers serialize the same reporting contract. Static-bake evidence is also a public core type; the conversion envelope remains a CLI producer contract.
  • Future serializers: no game-industry standard exists for skeletal animation lint results. Keep native JSON as the source of truth, then add serializers where downstream tools expect them: SARIF for code scanning, GitLab Code Quality/CodeClimate for MR widgets, JUnit XML for CI test dashboards, and CSV/HTML for humans.
  • diff A B: compares measurement maps per metric with fixed public per-metric significance thresholds, independent of configured lint caps; prints deltas; exits 1 on significant movement. Primary use: "did this DCC re-export change anything that matters?"

9. HTML report (the visual preview)

animsmith report clip.glb -o report.html → one self-contained offline HTML file (CI-artifact- and PR-attachment-friendly; no CDN, no install).

The explicit comparison spelling is a separate presentation producer: it preflights named/indexed skeleton correspondence, selected metric-grid work, and input text before either side's checks run; writes atomically only to an output distinct from both inputs; and uses one shared coordinate/path scale. Both judged poses are drawn through that one camera: side by side by default, and at the reader's option overlaid in a single pane with the after skeleton dashed over the before one at the same two selected frames. The overlay is a second reading of the embedded grids — no second copy of the pose data, no refit, and no new authority. Its phase synchronization labels both source times and never claims a retime. Findings, coverage, prediction provenance, and immutable identities remain side-specific, so a missing finding cannot read as acceptance.

Key design choice: no three.js, no <model-viewer>.

  • <model-viewer> can play a GLB but exposes no skeleton/per-frame API — it cannot draw bone lines, foot trails, or sync to charts. Fails the requirement outright.
  • three.js (~650KB inlined + GLTFLoader) works, but it would re-sample the animation in JS, and its loop/slerp behavior may disagree subtly with what the linter measured — the preview could contradict the findings it illustrates. It is also an update treadmill.
  • The decisive observation: the report never needs to sample animation in JS. The Rust side already computed the PoseGrid — model-space joint positions for every frame the checks judged. Embed that.

The source viewer is a hand-written WebGL2 skeleton renderer: bones as line segments, joint dots, root-motion and foot trails, orbit camera, play/scrub transport. It renders exactly the frames the checks measured — when loop-seam flags the wrap at frame N, the viewer scrubs to that frame N. Determinism is the feature.

  • Embedded data: pose grids as base64 Float32Array in <script type="application/json"> blocks (~60 bones × 3 floats × 300 frames ≈ 290KB base64 per clip; f16 quantization is the escape hatch if reports grow). The source GLB embedded once as a download button.
  • Charts: Rust-generated inline SVG — root-motion top-down path, foot heights, L−R gait signal with the fitted fundamental, per-bone seam-delta bars — with a small shared JS playhead syncing a cursor line across all charts and the 3D view. A declared gait_groups blend ring adds one cross-clip figure per group: every member's L−R foot height on one unshifted normalized source-phase axis, each measured stride anchor marked at its own phase, and the group's max_gait_phase_spread drawn as a band around their circular mean. The members are never aligned to their anchors, because that would erase the disagreement the figure is evidence for, and no member is nominated as the reference the others are judged against — the check judges the set's spread and names none.
  • Findings panel: each finding links to its clip + time; clicking scrubs the viewer.
  • Two clips side by side: the with control (and the with=NAME fragment key) plays a second clip of the same document beside the selected one, as two scissored halves of the one canvas drawn through one camera fitted to both clips' bounds. The transport drives the selected clip, and every half — its pose, its chart playhead and its path dot — is placed at the frame nearest that phase in its own grid, with both source times labelled. Both viewers place their two timelines through that one shared mapping and label it with one shared disclosure sentence. Like the comparison's shared phase it is a presentation mapping between two timelines, not a retime: the source panes select the samples the checks judged and never resample authored clip time.
  • Illustrative local blend: an optional third pane in the single-document report interpolates dense Rust-sampled local TRS, then runs full affine FK. Its continuous weight uses T/S lerp and shortest-hemisphere quaternion nlerp in binary64; exact endpoint weights reuse the source positions. A private binary64 Rust reference supplies synthetic conformance fixtures. No authored key sampler, rest-pose duplicate, all-pairs table, core measurement or engine runtime claim is introduced. The existing shared camera/orbit/zoom remains; interior poses may extend beyond its initial source bounds. The added local authority has a 32 MiB aggregate raw-byte cap plus clip, bone and metadata limits. Aggregate excess omits all local streams; invalid clips omit their own stream. Neither changes source findings, source playback or the public infallible render API. Evidence-only embeds neither positions nor locals. The output guide owns the detailed math, limits, omission and presentation contract.
  • A skinned-mesh view (vendored three.js, --report full) is a P2 option the crate layout leaves room for; it is presentation polish, not v1.

10. FBX ingestion (animsmith-fbx + convert)

  • Library: the official ufbx Rust bindings (v0.11.x, actively maintained; the same C foundation the incubating pipeline already trusted, so behavior is already trusted in the incubating pipeline).
  • Normalization at load via LoadOpts: target axes = glTF convention (right-handed, +Y up, −Z forward), target_unit_meters = 1.0 (FBX defaults to cm), transform-adjust space conversion (don't rewrite geometry), helper-node handling for 3ds Max geometric transforms. The core model only ever sees glTF-convention data regardless of source.
  • Animation extraction uses ufbx's bake_anim — it evaluates anim stacks/layers, cubic/TCB curves, pre/post-rotation, and inherit-scale modes into resampled TRS keys (rate from the FBX TimeMode, overridable). Each anim stack (take) becomes one core Clip. This sidesteps the entire FBX-curve-semantics swamp.
  • Scale inventory without enablement: the same successful parse produces a deterministic ufbx-side status for every current Appendix D.4 domain and a documented normalized source-node/source-skin projection when every declared skin slot can be represented. A missing cluster bone downgrades that generic projection to unavailable instead of silently dropping the slot. It explicitly names coordinate/inherit normalization, baked and lost curve state, generated normals/helper nodes, cluster-derived bind provenance, four-influence truncation/renormalization, rejected negative/non-finite/unrepresentable influences, triangulation/welding, omitted face/edge payloads, uninstanced mesh definitions, unsupported source data, and ufbx source identities. Only successfully projected cluster binds count as bone-convenience overwrites. One unreadable cluster makes its ordered bind declaration atomically unreadable; finite prefixes are not retained under shifted slots. Stackless authored curves, point/line faces, and attached zero-face mesh definitions are present-but-unsupported rather than absent or rebuilt; the zero-face definitions retain an exact count and stable source identity. Retained normalized meshes keep one definition per stable ufbx mesh identity; shared source geometry produces multiple node instances that reference the same compact normalized definition. They keep their stable ufbx typed identity so source-skin attachment joins cannot shift when an earlier source mesh emits no primitive. Its core capability projection remains unsupported for ordinary operations; narrow FBX rest-bind instead stages and proves a private GLB under the dedicated v5 contract. whole-document remains unsupported, and no raw FBX span or artifact-preservation claim is made.
  • convert emits glTF 2.0 GLB: nodes + skin (computed IBMs) + one animation per clip with at least one writable track; mesh + weights when present; --animation-only to strip mesh. The glTF writer returns counts derived from the emitted artifact, which both convert and transform use for their summaries and to report source clips omitted because they have no writable tracks. Explicitly not an art exporter — no material fidelity promise. Both convert and transform share one load→write round-trip over Document (assets included), so geometry survives a transform pass and --animation-only clears it uniformly across input formats (it is the only lever that drops geometry). --bake-static-mesh-transforms is the separate opt-in path for static, unskinned, singly-instanced geometry: it removes the effective rest hierarchy transform by baking positions and normals under an identity root and reports each applied transform in conversion evidence. Unsupported animated, skinned, malformed/non-finite, singular/near-singular, or reflected input is rejected before output.
  • FBX pitfalls double as checks when linting .fbx directly: source unit ≠ 1m (warn even though we convert), Z-up source, namespace-prefixed bone names (profile matcher strips), default "Take 001" naming (feeds naming), baked-key explosion (feeds key-density), non-uniform inherited scale.

11. Roadmap

  • M0 — walking skeleton. Repo bootstrap: workspace, dual license, CI (fmt/clippy/test on Linux/macOS/Windows), CC0 test fixtures. Core model
    • sampler/FK; animsmith-gltf; inspect and measure --format json; the mechanical P0 checks (nan → constant-track).
  • M1 — reference parity. Rig profiles, TOML config, per-clip expectations; port loop-seam, frozen-bone, root-motion-speed, gait-phase/gait-group — golden-tested against the reference implementation's verified production numbers; lint with exit codes + stable JSON; adopt the reference project's mutation-test discipline (corrupt one field, assert the finding names exactly that field). the incubating project's measure port lands here: its sidecar tool becomes a thin wrapper over animsmith-core + animsmith-gltf (the embedder keeps its sidecar schema and hashing); locomotion_metrics.py and the animation half of measured_sidecar.py are deleted.
  • M2 — report, FBX, diff → v0.1.0 on crates.io. HTML report; FBX ingestion + convert; diff. The v0.1 bar for "usable by an unaffiliated team": README quickstart works on a raw Mixamo-style GLB with zero config (profile auto-detect), built-in mixamo + ue-mannequin profiles, sample animsmith.toml, versioned JSON schema doc, no incubator vocabulary anywhere in the public API.
  • M3 — the hard semantics. foot-slide (stance detection), in-place, bind-pose/axis-conventions, rotational/velocity loop seams, --deny-warnings, baseline/suppression file for teams adopting with a dirty back catalog.

12. Risks & open questions

  1. Foot-contact detection robustness (M3) is the only research-grade item — thresholds vary with rig scale and style. Ship opt-in at warn until tuned on a corpus.
  2. Test-asset licensing: Mixamo clips cannot be redistributed. Use CC0 rigged clips (Quaternius/KayKit/self-authored) + procedurally corrupted fixtures; budget M0 time for this.
  3. Sampling-semantics fidelity: engines differ subtly (cubic handling, sub-frame wrap). The doc pins the model — "glTF-spec interpolation on a uniform grid, wrap = (last, 0)" — and exposes grid fps as config, accepting it is a model of runtimes, not all of them.
  4. ufbx 0.x churn and thin docs.rs coverage: pin exact versions, treat the C library's docs as canonical, keep animsmith-fbx thin and swappable.
  5. Rest-pose truth: node TRS and inverse-bind matrices can disagree. Rule: IBM-derived rest is authoritative when a skin exists, node TRS otherwise, and disagreement beyond tolerance is a bind-pose finding.
  6. Report size on long/many clips: f16 quantization + per-clip lazy JSON blocks if it bites.
  7. Scope pressure toward transformation will come ("you detected the seam pop — just fix it"). The linter-first line is the identity; only convert, transform, and mechanical/lossless fix operations may mutate data.

Appendix A — naming decision record

Two decisions, both 2026-07-03 (the project was renamed the same day it was built, before anything was published):

  1. animlint was chosen first, on a linter-first scope with clip transformation explicitly out of scope. Rejected then: gltf-lint (glTF is the carrier, not the domain; permanently confusable with Khronos glTF-Validator's spec-conformance role; wrong the moment FBX input landed), animkit/clipkit/clipforge (existing projects), gaitkeeper (minor collisions; overemphasizes locomotion).

  2. Renamed to animsmith the same day, when transformation became first-class: the incubating pipeline needs hemisphere normalization, frame-range slicing, hold-extend, and gait-anchor rotation sooner than later, and its archived-FBX2glTF conversion step wants a maintained replacement. The naming record had said the lint-first name "only breaks if clip transformation becomes first-class" — that fork flipped, so the name followed before the first crates.io publish made it permanent. animsmith and all sibling crate names verified free on crates.io with zero GitHub repository hits. Lint remains the flagship subcommand.

Appendix B — prior-art map (first consumer)

The measurement algorithms, config vocabulary, and testing discipline were extracted from a private game project's asset pipeline — the uniform-grid sampler, the local-neighbour loop-seam denominator with its stride floor, the L−R fundamental-harmonic gait anchor, the root-motion speed gate, the Pinned{value, tolerance} expectation shape, the Finding/severity/exit-code conventions, and the mutation-test style (corrupt one field, assert the finding names it) are all faithful ports, golden-tested against that pipeline's shipped numbers. Its measurement scripts are deleted as the project migrates onto the animsmith library — the standing proof that the public API is sufficient for a real bake pipeline.

Appendix C — result-contract ownership decisions

These decisions record result-contract ownership after output v3 finalization:

  1. The versioned envelope belongs to animsmith-core, not the CLI binary. The CLI still supplies its build identity and frontend policy, but MeasurementContract, the command-specific MeasureFileReport and LintFileReport records, command-specific envelopes, summaries, and URNs are shared library types. This lets embedded producers emit the exact schema without copying private structs or protocol strings while making an invalid measure/lint record shape unrepresentable.

  2. Nested findings keep their check_id. A Finding is also a standalone embedded result and the record consumed by text, Markdown, and HTML presentations. Keeping it self-describing avoids a second wire-only finding projection and supports extracting findings without retaining their parent check record. Output v3 therefore accepts the small nested redundancy, and evaluate_checks rejects a child id that disagrees with its parent rather than serializing ambiguous ownership.

  3. Read-side validation is consumer-neutral. animsmith-core validates output and nested measurement identities and recovers every file's display path plus complete clip and mesh measurements in source order. It does not impose a file-count rule or prescribe CLI remediation. The diff frontend owns its single-file policy and operator guidance; embedded consumers may accept empty or multi-file reports according to their own workflows.

Appendix D — decision record: skinned rest/bind scale canonicalization

Status (2026-08-20): implemented for self-contained glTF/GLB, the narrow FBX rest/bind re-encode path, and character assembly recipe/evidence v7. The shared core plan, typed ledger, exact-source writers, independent core and artifact proofs, atomic CLI publisher, and immutable scale-evidence v4 producer are the current contract. Character assembly v7 can opt into rest/bind scale for glTF/GLB and inventory-complete FBX inputs with an exact root name and factor after validating a versioned basis for the base and every separate clip; recipe/evidence v1 through v6 remain unchanged. Whole-document conversion supports raw glTF POSITION morph deltas while preserving static JSON weights as numeric values and animated weight accessor payloads byte-exactly; other morph semantics and every rest/bind morph payload remain refused. FBX whole-document scaling remains deferred.

D.1 Problem and two distinct operations

A skinned mesh can already have the intended physical dimensions while its joint hierarchy carries a compensating uniform scale. For example, a root can contribute 0.01, descendant translations can be authored 100 times larger, and inverse-bind matrices can carry a compensating linear magnitude of 100. Skin deformation is then visually correct, but a rigid object parented to a joint inherits the unwanted 0.01 scale.

That case must not be confused with a document whose entire linear unit is wrong. The following are separate, explicitly selected operations:

  1. Whole-document linear-unit conversion changes physical size. The caller declares a finite factor q > 0; every represented length is converted by q. It is appropriate only when the source was authored in a different linear unit.
  2. Rest/bind hierarchy reparameterization preserves already-correct world geometry. It removes one compensating inherited scale from a restricted skinned hierarchy, rebases local rest and animation translations, and regenerates inverse binds.

Neither operation may infer its factor or applicability from mesh bounds, character height, joint lengths, inverse-bind magnitude, filename, or an asset category. Measurements from #267 are diagnostic evidence, not conversion authority. A caller must name the operation and declare or accept the exact factor that the transform plan validates.

The declared unit-conversion transform is a finite positive scalar only; it is not an axis, rotation, translation, shear, or reflection operation. The initial reparameterization source class is finite, orientation-preserving, positive uniform rest-world scale. Non-uniform scale, shear, reflection, singular or near-singular matrices, non-finite values, and mixed effective factors in the selected domain fail before any output is written.

Classification and proof share one versioned tolerance policy and compute in f64, narrowing only at the writer model boundary. The current policy identity is appendix-d-v6. The three finite widened axis lengths are sorted in ascending order before their arithmetic mean is accumulated and divided by three. This canonical association rule makes the common factor independent of authored axis order, including when the sum lies on an equal-axis boundary. Relative orthogonality, equal-axis, and common-factor tolerance is 1e-5; an axis is unequal when abs(length - average) > 1e-5 * max(average, length), relative to the longer of the two and to nothing else; a determinant is singular when abs(det) <= 1e-6 * product(axis_lengths); scalar/vector comparison uses abs_error <= 1e-6 + 1e-5 * max(abs(before), abs(after)); and shortest-path rotation residual is at most 1e-5 radians. Every bound is inclusive: a residual exactly equal to its bound is accepted. Exact float equality is forbidden. The implementation records every threshold and observed maximum residual in evidence, so noisy values such as 100.000015 can be accepted for an explicit, reviewable reason rather than by implementation accident.

This is a pre-1.0 policy-identity cutover: the former public ScaleTolerancePolicy::APPENDIX_D_V5 associated constant is removed rather than retained as an alias for the new axis-order-independent meaning. Evidence records therefore use appendix-d-v6. The historical v5 calibration and revision history live in the scale proof calibration notes; the recurrence v5 introduced and v6 retains is specified below as part of the current policy.

The f32-rounding term, per obligation. The scalar band above is stated relative to the quantity being compared. For five of proof's obligations that quantity is not the magnitude the arithmetic ran on, and a rotation can separate the two without limit: a bound component near zero on a mesh four thousand units across, a near-identity W * B whose translation column is the difference of two terms of magnitude abs(W), or a world translation whose parent chain cancelled two terms larger still. The compared number is then small and the absolute rounding error it carries is large, so a purely relative band derives the tolerance from the small number and the error from the big one — and plan_scale accepts a plan whose candidate prove_scale then refuses. Sweeping a rotating rig at a declared factor of 3190, that happened on 86 % of rotations before this policy, with residuals up to 9.8e-4 against a 6.1e-5 band.

appendix-d-v6 therefore retains the further quantity f32_rounding_ulps = 4, and the five obligations that compare f32-rounded arithmetic use

abs_error <= 1e-6 + 1e-5 * max(abs(before), abs(after))
             + f32_rounding_ulps * magnitude * 2^-23

where magnitude is named per obligation below. The added term is absolute in magnitude, never relative to the compared quantity, which is what makes it incapable of loosening a comparison whose operands already are its own magnitude: there it contributes 4 * 2^-23 = 4.77e-7 of them, twenty times below the 1e-5 the relative band already allows. The 2^-23 is f32::EPSILON, because every one of these residuals is the difference of two f32 quantities even though the subtraction itself is done in f64.

  • Skinned bounds takes magnitude as the largest per-vertex arithmetic provenance over every contributing vertex — read off the candidate, and maxed against the source's own rebased by the conversion factor: candidate.max(q * source), for the reason given after this list. Each influence has three stages whose cancellation a later stage cannot recover:

    1. the magnitude the W * B * p transform runs on, which is max over i of sum over k of abs((W * B)_ik) * abs(p_k) with p extended by the homogeneous 1 that transform_point3 sums the translation column in with — the product abs(W * B) * abs(p) and not either factor alone — because the weighted sum over slots that follows can cancel those terms against each other;
    2. the magnitude the contributing slot's W * B composition ran on, which covers the cancellation that composition performs; and
    3. the magnitude that slot's joint world translation was itself accumulated from along its parent chain, which covers the cancellation performed one composition earlier.

    For one influence, the base is the maximum of those three stages. A vertex combines those influence bases with the same binary64 weighted average of the stored non-negative binary32 weights as its skinned point: sum(w_i * base_i) / sum(w_i). The accumulator then takes the maximum vertex base. All three stages are load-bearing, and each dominates the others by an unbounded ratio on some rig. The skinned points alone miss a joint placed far from the geometry it carries — a 1000-unit local translation under a 3190 root puts the joint 3.2e6 from the origin while its vertices skin to within one unit of it, and the bound inherits 1.6e-1 of error from a 9.7e-1 extreme. Dropping the slot-composition stage makes the calibration sweep refuse correct candidates in fifty-six of its seventy-two pre-v4 cells, at up to 62.6 ulps of the base that remains; dropping the transform stage outright refuses in thirty-four, and narrowing it to abs(p) alone refuses in fifteen at up to 47.7.

    Stage 1 has been wrong twice, in the same place. It first read the skinned point's magnitude and claimed that covered the transform; the correction read the vertex position abs(p) and claimed the same. Both name one term of a product of two, and each was exact only on the rigs that happened to be built.

    • The skinned point coincides with the transform only while no two slots oppose. Two slots whose composed W * B differ by a half turn send a vertex 1000 units out to a blended origin, so the result reads 0 while every term summed to produce it carried that vertex's own ulp; put the joints near the origin as well and stages 2 and 3 read 1. The residual is then 6.1e-5 — one ulp of 1000 — against a 1.5e-6 band, a demand of 503 ulps, rising to 65_527 at abs(p) = 1e5.
    • abs(p) coincides with the transform only while abs(W * B) is 1, and the one rig that reached the cancellation composed W * B from a pure rotation, where it is 1 by construction. Give those same two slots a uniform scale of k and the sum still cancels while each term carries the rounding of a k * abs(p)-magnitude transform: at k = 1024 and abs(p) = 65536 the residual is 4.0 against a 0.0313 band, a demand of 512 ulps, and the rig refuses from min(k, abs(p)) > 8 upward with the shortfall growing as min(k, abs(p)).

    Both need only that the bind pose is not the rest pose, which is ordinary content: with W * B the identity on every slot there is nothing for a blend to cancel and nothing but 1 for abs(W * B) to be, which is why a section's worth of fixtures built on analytic inverse binds could express neither case. two_slots_whose_composed_binds_cancel_a_vertex_still_prove_its_bounds pins the cancellation and two_slots_with_a_scaled_composition_cancel_a_vertex_and_still_prove_its_bounds pins the second factor; both build their binds independently of the rest world for that reason, and the calibration sweep carries composed slots at abs(W * B) = {1e-3, 1, 1e3} so a third factor of the same kind is visible to a measurement rather than only to a reviewer.

    The blended point is not another stage. Shared scale-input validation refuses every finite negative primary skin weight before planning. The implementation widens the stored binary32 weights, transformed-point numerator, and denominator to binary64, divides once, and narrows the final point to binary32. This prevents a lone subnormal contribution from disappearing before division, several large finite weights from overflowing their denominator, and rounded binary32 coefficients whose sum exceeds one from overflowing an otherwise finite convex blend at f32::MAX. Bounds residuals are compared per axis, so each blended component is already bounded by the same binary64 weighted transform operands. v3's L2 length could be up to sqrt(3) larger while naming no additional per-axis arithmetic; v4 removes it.

    This validation decision removes two formerly unbounded affine cases. With weights 1.0 and -0.99999, a positive near-zero denominator could amplify opposed transformed points to 1.997e8; with identical transformed points, both numerator and denominator could cancel while the accumulation error was still divided by the near-zero sum. No finite ULP count could cover the latter generally. The two #336 counterexample rigs remain as fixtures and pin NegativeSkinWeight at the shared planning boundary instead of forcing tolerance machinery to describe invalid input.

    Weighting the provenance is equally load-bearing in the other direction. A stage at magnitude 1e20 with weight 1e-20 contributes about 1, not 1e20; an unweighted max would let an influence with essentially no effect buy the whole distant stage's tolerance. Separate named fixtures isolate the transform and slot-composition halves, a proof-level fixture measures the old and v4 detection floors, and the calibration has an explicit profile that gives each vertex's larger production influence base the smaller weight.

    The proof-level floor is an adjacent-binary32 bracket on the same candidate-only weight mutation in both policies. A frozen v3 evaluator first refuses at candidate weight bits 0x3a7a1be4 (the preceding 0x3a7a1be3 is accepted), where the Bounds residual is 9.5320709005312e13. v4 searches both directions around the authored 1e-20 weight; its smaller refused residual is 2.4400651454925537e-6, with the lower bracket 0x1e3c6f0a refused / 0x1e3c6f0b accepted and the upper bracket 0x1e3d5b21 accepted / 0x1e3d5b22 refused. That is a 3.906e19x recovery in this adversarial rig. It is a measured detection floor for this fixture, not a universal precision claim, and the exact bits and residuals are pinned in the test.

    magnitude is deliberately not read off the bound corner being compared. A per-axis extreme is contributed by whichever vertex happened to be furthest along that axis, so three vertices at (3000, .001, .002), (.001, 3000, .003) and (.002, .003, 3000) build a corner of magnitude 2.4e-3 out of vertices of magnitude 3000. A corner-derived band is a million times tighter than the error the corner carries, and — read the other way, as a base for the relative term — a corner-derived tolerance admits 4.2e-2 on a component of magnitude 5.98, which is 0.71 % relative error and no longer a rounding allowance at all.

  • The skin equation and the inverse binds of an unaffected instance take magnitude as the largest quantity any entry of the compared product was summed from: max over (i, j) of sum over k of abs(a_ik) * abs(b_kj). For W * B that is 6.4e3 on the rig above, not the near-identity product's 1.0: the residual there is 1.8e-4 against the 1.1e-5 band the product magnitude buys, a 16x shortfall, and 2.50 ulps of the operand magnitude after it. The skin equation maxes that against the same parent-chain magnitude skinned bounds takes, because abs(W) * abs(B) reads the already composed W and so cannot see terms the chain that produced it already cancelled. A joint whose local offset points back along its parent's world translation leaves abs(W) * abs(B) at 1.0 while the residual it must admit is 6.25e-2 — 524288 ulps of that base, and 0.08 of the chain's. The skin equation reads that quantity off the candidate and maxes it against the source's rebased by the factor, exactly as skinned bounds does, and for the same reason. It is also not matrix_magnitude(a) * matrix_magnitude(b), which replaces the sum over k with a product of two independent maxima and reads 7.6e6 where the arithmetic ran on 6.4e3 — a tolerance from that would accept a matrix that is entirely wrong, and the over-acceptance calibration fixture refuses exactly that case.

    For an unaffected instance's binds the two operands are the effective matrix and the declared factor, and scaling a column cancels nothing, so the term is inert there: a correctly built candidate leaves the effective bind unchanged and therefore has exactly zero residual. It is stated rather than omitted so that the policy quantity means one thing across every obligation that compares f32 matrices.

  • Rest translation and sampled trajectory compare a node's world translation between the two documents, and take magnitude as that node's parent-chain magnitude alone — the same quantity the two above max against, read off the rest pose and off the sampled pose respectively. There is no product to sum here: the comparison is between two composed world translations, and the only way one of them can be small while its error is large is the chain cancellation.

    The root carries zero provenance because its world is a copied local. For child i, with parent world W_p, local matrix L_i, and inherited provenance C_parent, the shipped binary64 recurrence is

    s_i,r = sum over k in {0,1,2}
            f64(abs(W_p[r,k])) * f64(abs(L_i[k,3]))
    p_i,r = f64(abs(W_p[r,3]))
    m_i,r = s_i,r + min(max(p_i,r, f32::MIN_POSITIVE), s_i,r / 2^-23)
    m_i = max over spatial rows r of m_i,r
    C_i = C_parent + m_i
    

    The first term provisions the local three-product dot. The second provisions the final addition by paying the smaller of one ulp at the carried parent's scale and the whole new contribution being absorbed. The minimum-normal floor covers subnormal product rounding without imposing a fixed charge when s_i,r = 0. Only the three spatial output rows participate; affine row 3 is the exact homogeneous coordinate. Rest and sampled pose construction share this recurrence. Its calibration populations and revision history are recorded separately in the scale proof calibration notes. The policy-neutral matrix rewrite, residual, and provenance kernels live in crates/animsmith-core/src/scale/numeric.rs; operation selection, tolerance decisions, reference expectations, connector products, and proof-owned expected values remain outside that leaf.

    This scalar is not a universal componentwise forward-error proof: it does not separately propagate error in the inherited linear block before that block acts on a later translation. The shipped claim is this additive recurrence and its checked empirical envelope, not an unbounded-depth analytic guarantee. At the later fixed stages, SkinMatrix maxes the accumulated chain against the W * B operand magnitude and Bounds feeds that slot base through the weight-proportional blend described above. A fully analytic matrix-error propagation would be a different model and policy contract.

    The chain is the candidate's, not the source's and not the max of the two. Whole-document conversion scales every translation by the factor and leaves every linear part alone, so the two documents' chains are that factor apart — subject to the candidate's f32 narrowing of the factor, since the build scales by factor as f32 while the proof rebases by the f64 factor, a relative difference of at most 2^-24 (1.49e-8 at q = 0.1, whose f32 is 0.10000000149011612) that this very rounding term covers many times over — and the source's rounding is rebased by the same factor before it is compared, so the residual scales with the candidate's chain at either end of the factor range, and under rest/bind the two chains are equal outright. A max over the two sides is therefore never needed, and under a shrinking conversion it is strictly worse: at a factor of 0.01 the source chain is 100x the candidate's, so the max freezes the band at the source rig's size while the candidate it is spent on keeps shrinking, and the smallest genuinely wrong candidate either obligation refuses stops tracking the factor at all. On the cancelling-chain rig that costs 89x of discriminating power at 0.01 and 648x at 1e-4. a_shrinking_conversion_holds_rest_translation_to_the_candidate_s_own_chain and a_shrinking_conversion_holds_trajectory_to_the_candidate_s_own_chain pin the tightening; the two ..._holds_..._to_the_candidate_side fixtures pin the growing direction, where the candidate's chain is also the larger.

Transform-only affine is not in this rounding class. It compares a probe point transformed through the complete expected and actual world affines as a vector L2 residual against a vector-magnitude base, so its comparison base is already the magnitude its arithmetic ran on and it takes no rounding term.

Overflow is not NaN. A skinned position that leaves the f32 range is reported as InvalidSkinnedPrimitive { reason: "skinned_magnitude_overflow" }, separately from the "non_finite_result" reported for NaN. Both fail closed, and neither has a fixed preflight magnitude limit: skinning accumulates a dot product per axis, so overflow depends on orientation and operand magnitudes, not only on the magnitude of the result.

The magnitude calculations follow the same finite-domain rule. Bounds are compared per axis and never square a finite blended point into an L2 length that could overflow above sqrt(f32::MAX). The parent-chain abs(W_parent) * abs(t_local) sums are accumulated in binary64. Composition and column operand magnitudes are summed in f32 lanes and recomputed in binary64 whenever that sum leaves the f32 range, so an operand magnitude is never reported as inf while the composed result remains finite. These are current appendix-d-v6 proof and refusal semantics, not calibration measurements.

Why the source side is rebased. Every residual above is |a - q * b| for the candidate quantity a and the source quantity b, so the source operand enters the comparison multiplied by the factor and its rounding is multiplied by the factor with it: a source quantity accurate to k ulps of its own magnitude contributes q * k ulps of that magnitude to the residual. A base that reads the source side unrebased — which max(source, candidate) did for skinned bounds and the skin equation — therefore states the source's error in the wrong units by a factor of q. Under a shrinking conversion that is the whole defect, and it is loose by exactly 1/q: the band freezes at the source rig's size while the candidate it is spent on keeps shrinking. Measured on the far-joint rig, the smallest inverse-bind shift the skin equation refuses goes from 1.9e-3 under the max to 1.3e-5 at 0.01 and 1.3e-7 at 1e-4; the smallest bounds error refused goes from 4.8e-1 to 4.8e-3 and 4.9e-5. a_shrinking_conversion_rebases_the_skin_matrix_magnitude_by_the_factor and a_shrinking_conversion_rebases_the_bounds_magnitude_by_the_factor pin both.

Unlike the parent chain this does not reduce to the candidate's magnitude alone, because these two magnitudes are products: only the terms carrying a translation are a factor apart, and both sides retain an unscaled O(1) floor from the composition's linear block and from the exact 1.0 the homogeneous row contributes to every chain. Where that floor dominates the source, q * source exceeds the candidate's magnitude under a growing conversion by up to the whole factor. Production retains the max because it is the operand bound justified by the comparison; no smaller candidate-only or min base is part of the policy contract.

An earlier calibration tried to classify those smaller bases as equivalent by dividing an obligation-wide maximum residual by a separately selected maximum slot base. Those maxima can belong to different slots: after calibration began recording each production comparison's own residual and provenance, the shipped SkinMatrix demand was 2.213 while that mispairing still printed 2.084. The alternative-base figures and equivalence claim were removed rather than preserved as false mutation evidence.

Calibration evidence. The checked-in release calibration, its deterministic populations, current reference demand tables, historical policy comparisons, and machine-local cost measurements live in the separate scale proof calibration notes. Those notes measure this policy; the normative thresholds, provenance definitions, and refusal boundaries remain in this decision record.

Normative residual norm for the unit-scale postcondition. The postcondition "unit composed scale for every affected node" is measured per axis, as max(abs(scale_axis - 1)) over the three axes — an L-infinity norm, not an L2 norm over the three axes together. This is normative. Per-axis is what the claim means, it is how the equal-axis check already measures, and it is the same dimensionless relative quantity the scalar common-factor check compares, so the input band and the postcondition are directly commensurable.

How the common-factor band relates to it. The common-factor band is the normative input contract, and the unit-scale postcondition bound is derived from it rather than declared independently. For an affected node i with observed rest-world factor s_i, a domain common factor s_0, and a declared factor s_declared, the candidate's composed scale on axis k of node i is axis_ik / s_declared. Three independently bounded relative comparisons stand between those two numbers, and an affected chain composes all three:

  • the declared-factor match binds s_0 to s_declared within c = 1e-5;
  • the mixed-factor check binds every other affected node's s_i to s_0 within the same c; and
  • the equal-axis check binds each individual axis length axis_ik to s_i within the same c.

The third band is the easiest to lose sight of and cannot be dropped: s_i is the average of node i's three rest-world axis lengths, while the postcondition measures an individual axis, so an axis is permitted a further band away from the very number the mixed-factor check compared. A rig whose root is off-factor, whose leaf is off-common-factor, and whose leaf axes are non-uniform within the equal-axis band loads all three at once.

Each comparison is stated relative to max of its operands, so each contributes at most c / (1 - c) when re-expressed relative to the smaller one, and the composed worst case is (1 - c)^-3 - 1 = 3.00006e-5. The policy therefore sets the postcondition bound to four common-factor bands — 4e-5 — rounded up to the next power of two, 2^-14 = 6.103515625e-5. Three of those bands are the analytic composition above; the fourth is headroom for the f32 world-matrix composition and decomposition that produces the measured composed scale. 2^-14 is also 512 * 2^-23, and so lies on the binary32 mantissa grid the measurement lives on, which is what makes the inclusive "at most" above reachable rather than vacuous for this obligation.

Three bands rounded up (2^-15 = 3.0517578125e-5) would leave that analytic worst case only four binary32 ulps of room — 5.17e-7, or 4.34 * 2^-23. That is a rounding artefact, not headroom, and it would make the reserved float-headroom band above a claim rather than a fact. The fourth band buys the claim honestly, and it does not blunt the obligation: every build defect this postcondition exists to catch — a dropped rebase, a factor applied twice, a stale no-op candidate — is at least 1e-3, so 6.1e-5 still leaves better than a 16x detection margin.

No floor on the comparison base. The common-factor and mixed-factor comparisons are abs(a - b) <= c * max(abs(a), abs(b)), with nothing else inside the max. Flooring that base — at 1.0, at the 1e-6 absolute scalar term, or at anything else — turns the band into an absolute tolerance below the floor, which is a relative band of floor * c / abs(s) that widens without limit as the declared factor shrinks. It therefore breaks the closure property below floor * c / 2^-14; at a 1e-6 floor that is 1e-11 * 16384 = 1.6384e-7 (the crossing point is inversely proportional to the postcondition bound, so the 2^-15 bound an earlier revision declared put it at twice that, 3.2768e-7), and at a declared factor of 1e-9 such a band admits 1e-2 relative error, 1000x the declared policy. The degenerate a == b == 0 case needs no floor either: it compares 0 <= 0.

What the derivation guarantees is the closure property: any plan the common-factor band accepts yields a candidate whose every affected node satisfies the unit-scale postcondition — at every declared factor the policy admits, not merely near unit magnitude — with a full band to spare (6.1035e-5 - 3.00006e-5 = 3.1035e-5). The guarantee is stated for the single closed affected domain this record admits — one common factor, one declared factor, hence exactly the three composed bands above. Admitting a second independently classified factor in one plan would compose a fourth band and require a new policy identity, not a wider bound at this one.

Sampling budget. The policy also bounds the total sampled proof work a document may demand, as sample_time_count * per_sample_work_units against a budget of 4e8 work units. The per-sample cost is every pass the sampled obligations actually make, and each is charged once per document side, since proof walks the source and the candidate:

2 * bone_count
  + sum over affected skinned instances of
        [skin_and_bounds] *
          (3 * len(skin_joints)
           + 2 * (vertices over every primitive of its mesh))

Every sampled obligation poses both skeletons, hence the bone term. Only the source bone count is measured, which is sound because proof rejects a candidate whose bone count differs (bone_count_mismatch) before it charges anything: the candidate document is caller-supplied, so an unchecked candidate skeleton is proof work nothing has billed. The slot term is charged explicitly, per instance: nothing bounds the instance count, and nothing bounds skin_joints, which may repeat a joint, so slot work bears no relation to the bone count and must not be folded into it. The vertex term charges each vertex once, while the f32-rounding stage of the skinning walk runs once per non-zero influence. A four-influence vertex therefore performs that stage four times for the same vertex charge; the budget is the fixed released work proxy calibrated for the documented populations, not an exact instruction counter. sample_time_count counts key times and cubic-segment interior times, because both are evaluated. Exceeding the budget is a typed refusal raised before the first sample time is evaluated; proof never silently samples a subset. The comparison is >, so the budget is a ceiling a document may reach, matching the inclusive "at most" every other policy quantity is stated with. The budget is part of the policy identity for the same reason every tolerance is: it is recorded in evidence, and two evidence records carrying the same policy id must describe the same amount of checking.

The calibration notes record the historical timing shapes and measurements that established this charge as a linear proxy for real work. Those seconds are machine observations, not a normative wall-time guarantee.

The value has to clear real assets as well as bound bad ones: a 200-bone rig with a 100k-vertex skinned mesh costs 201_000 units per sample time, so a 30-second clip at 30 fps costs 180_900_000. A budget that refuses that refuses a plausible production asset, with no way for a caller to opt into the work.

Historical implementation cost and baseline comparisons are retained in the scale proof calibration notes, not in this normative work-budget definition.

D.2 Algebra and rewrite rules

Let L_i(t) and W_i(t) be node i's local and composed world matrices, G be a skinned primitive's geometry-bind-to-world matrix, and B_i be its inverse bind. Valid input satisfies

W_i(rest) * B_i = G

for every joint slot. Matrices below act on column vectors.

Whole-document linear-unit conversion

For the caller-declared factor q, define U = scale(q). Coordinates change basis as M' = U M U^-1. For a uniform U, this multiplies an affine matrix's translation by q while leaving its dimensionless linear part unchanged. Therefore:

  • multiply every node-local rest translation by q;
  • multiply every animation translation value and both glTF cubic-spline translation tangents by q; key times remain seconds;
  • multiply mesh POSITION values and morph POSITION deltas by q;
  • conjugate every per-skin inverse bind as B_i' = U B_i U^-1;
  • multiply every supported camera, light, collision, or extension length by q through a field-specific handler; and
  • leave rotations, normals under positive uniform conversion, UVs, weights, times, morph weights, and rest/animation scale channels unchanged.

glTF LINEAR and STEP samplers have values only; they do not store tangent elements to rewrite. “Translation tangents” throughout this record therefore means the explicit in/out elements of a CUBICSPLINE translation sampler.

Root-motion positions and velocities are not separate stored fields: they are recomputed from the converted translation tracks. Distances and velocities therefore change by q; durations do not. Animation scale is dimensionless and is never multiplied as though it were a distance.

This operation preserves topology and semantics while intentionally producing world positions q times the source positions. It is not a repair for an already-correct mesh with a compensating skeleton scale.

Rest/bind hierarchy reparameterization

For the initial supported class, every affected joint has a rest-world linear part within tolerance of s R_i, where s > 0 is one common factor and R_i is a proper rotation. The affected domain is a closed connected hierarchy: the scaled ancestor, all selected skin joints and paths between them, and all descendant nodes whose attachment transform would otherwise inherit s. A strict source-node connector with no normalized bone may occur on a path between two projected nodes when its only modeled contribution is its authored static local rest, which must be finite and affine. Planning composes that local in source order; the connector does not become an affected normalized node, its authored local is preserved exactly, and it owns exact-preservation and projected-successor bridge obligations rather than a numeric residual. The selected root and every selected skin joint must still project to normalized bones. An unprojected descendant or attachment that would itself need rewriting, a second effective factor, or an instance outside the closure still rejects the plan.

The compiled rest/bind plan records this source topology once, keyed by raw source-node identity. A projected row also records its normalized BoneId, authoritative raw parent, nearest projected parent, role, and the incoming connector tail from which the ordered span is derived. A connector topology row records its source and parent identities; field rows state its exact preservation, and the projected successor identifies ownership of the bridged rewrite. Derived indexes may accelerate lookup, but they are not additional topology authorities. This distinction matters to embedders: source-node indices select and preserve raw structure, while BoneId identifies normalized rewrite and residual domains. The same global table also retains OutsideDomain rows, including every source row for whole-document conversion, so raw parent/projection identity is never inferred from absence.

For affected node i, define the constant basis correction C_i = scale(1 / s_i), where the initial contract admits s_i = s inside the domain and s_i = 1 at its parent boundary. The desired world matrix is

W_i'(t) = W_i(t) * C_i

and the corresponding local matrix is

L_i'(t) = C_parent^-1 * L_i(t) * C_i .

The same rule remains valid across a preserved static connector, but it does not reduce to skipping that connector or blindly applying the projected parent's scalar to the next raw local. Let H be the ordered product of unprojected connector locals between projected parent p and projected node i, and let L_i be i's authored local after that connector span. Since H is preserved, the projected successor's authored local must become

L_i' = H^-1 * C_p^-1 * H * L_i * C_i .

Then W_p' * H * L_i' = W_p * H * L_i * C_i, as required. A translated connector makes the conjugation's additive translation term observable, so a non-identity connector fixture is required; an identity-only fixture cannot distinguish composition from omission. This is a static-rest rule only: SourceNodeAsset carries no raw animation channel for an unprojected row, so support for a frontend that drops an animated connector requires a separate source-animation inventory and rewrite contract.

Because every admitted C is a positive uniform scale, the result remains representable as TRS. On a direct projected edge (H = I) — including the normalized Bone local into which a loader has already folded a connector — the node-local translation is multiplied by the parent's s_parent, rotation is unchanged, and local scale is multiplied by the dimensionless ratio s_parent / s_i. For a raw projected successor after a preserved connector, the linear part receives that same ratio but its translation is instead determined by H^-1 * C_p^-1 * H * L_i * C_i; a translated H contributes an additive term, so the simple translation multiplier must not be applied to that raw local. The connector product, inverse, additive term, complete bridged translation sum, and bridged linear ratio application are evaluated in f64, then each final source-local component is narrowed once to the f32 model boundary. Direct projected edges retain their established f32 arithmetic and association. Planning likewise keeps each admitted connector span widened until its projected endpoint, so a built candidate that retains Complete source projection remains a consumable factor-one planner input even when the preserved connector-only product exceeds f32. Translation animation values and both cubic translation tangents on normalized nodes receive the existing parent-basis multiplier. Scale animation on those normalized nodes is the same local-scale component over time, so every stored VEC3 scale element receives s_parent / s_i too: the selected closure root receives 1 / s, a strict affected descendant receives s / s = 1, and an unaffected node receives 1. This is a topology rule, not a constant-track predicate: LINEAR, STEP, and every value plus in/out tangent of CUBICSPLINE are rebased. Rotation tracks and key times remain unchanged.

A node on a direct projected edge that declares matrix rather than TRS members takes the same rule in component form. Writing L_i' out in glTF's column-major order, the nine linear entries (0,1,2, 4,5,6, 8,9,10) are multiplied by s_parent / s_i and the three translation entries (12, 13, 14) by s_parent. Components 3, 7, 11, 15 are written back exactly as authored, which is the correct rebase only because the §D.4 gate has already proved them exactly (0, 0, 0, 1): the basis change multiplies 3, 7, 11 by 1 / s_i, a no-op on an exact zero and on nothing else, and leaves 15 alone at any value, so what the gate buys there is not the arithmetic but the premise — a matrix is the node's whole transform, with no projective divide this record models, only when 15 is exactly one. §D.3 case 4 records why that gate compares exactly rather than within tolerance. A raw projected successor after a preserved connector is governed by the full H product above, not this component shortcut.

Mesh positions, morph deltas, and normals remain unchanged because their world geometry is already correct. Each inverse bind is regenerated from the unchanged geometry bind:

B_i' = inverse(W_i'(rest)) * G = C_i^-1 * B_i .

Consequently W_i'(t) * B_i' = W_i(t) * B_i: the complete skin palette, not merely joint origins, is analytically preserved.

Both bone convenience inverse binds and every per-instance skin-slot array must be updated; the single bone-level value is never authority for a multi-skin document. A raw format has one bind store per skin and no per-node bind, so the frontend rewrites each affected skin's stored matrices with per-slot factors and proves through the reloaded document that every per-instance array and bone convenience value is correspondingly correct. The output deliberately changes the affected nodes' composed scale to one while preserving their world translations and orientations, animation trajectories, skinned vertices, and bounds.

Conflicting raw accessor claims reject. Unlike whole-document conversion, whose converted uses all take the same q, rest/bind derives a typed claim for every raw accessor use, not only a scale-bearing write. A claim records the accessor's expected component shape and its multiplier. Translation outputs, scale outputs, and inverse-bind slots carry their topology- or slot-derived multiplier. Factor-one preservation claims cover mesh attributes and indices, sampler inputs, rotation and weights outputs, and sampler outputs that no channel selects. The latter are still sampler-referenced payloads, not unused bytes that the raw proof may overlook.

One accessor index must have compatible component shape and one multiplier across all of those claims. Thus a translation or scale output reached from the closure root and from a child, from an affected and unaffected node, or an accessor used both as mesh POSITION and a translation or scale output can reject; so can a same-index type conflict even when both factors are one. One inverse-bind store shared by skins whose joints straddle the closure is the same problem. Splitting the payload is not a remedy, because it changes array lengths and destroys the identities the proof pins.

Distinct accessor indices are checked too: if their resolved byte ranges overlap and either claim rewrites bytes, the operation refuses rather than silently corrupt a preserved neighbour. Direct-image bufferView consumers are not accessors and have their own separate image-overlap refusal. Within the source domain admitted by the common conservative capability preflight, uses whose claims agree impose no additional rest/bind restriction however many reach one accessor. The preflight can still refuse a scale-bearing/dimensionless alias or overlapping raw range before this ledger runs. A declared factor of one makes every rest/bind multiplier one, so this ledger's factor disagreement cannot fire on a no-op; it does not broaden the preflight's supported raw domain.

D.3 Worked synthetic cases

The implementation fixtures use literal analytic values rather than values generated by the transform under test.

  1. Unit skeleton. A root and child both have unit scale, the child rest translation is (0, 1, 0), and the IBM is rigid. Reparameterization with an expected factor of one is a deterministic no-op. Requesting any other factor rejects as a source-fact mismatch. A whole-document conversion by 0.01 instead produces a 0.01 m child offset and mesh while leaving scale channels at one.
  2. Compensated inherited scale. A root has scale 0.01; its child has local translation (0, 100, 0) and local scale one. Their composed child position is (0, 1, 0), every selected joint has effective scale 0.01, and the corresponding IBM linear columns have magnitude 100. The reparameterized root and child have unit composed scale, the child local translation is (0, 1, 0), the IBM is rigid, and mesh positions and the child world position stay byte-for-byte or tolerance-equivalent as defined by the proof. A transform-only child at local offset (1, 0, 0) is rebased to (0.01, 0, 0), preserving its world origin while its composed linear scale changes from 0.01 to one. Transforming a further off-origin point through that child's complete affine distinguishes the result from a no-op. Checking only joint_translation + joint_rotation * offset is invalid because it drops the very scale channel the fixture must prove. Unskinned geometry in the affected closure is initially rejected rather than silently resized; supporting it would require a separate geometry and instancing rule.
  3. Separate clips. A compatible clip declares the same named parent topology, rest orientations, helper layout, and 0.01 translation basis; its child translation value and cubic tangents are rebased by 0.01 before remapping. A name-identical clip with a unit parent basis, different parent rotation, different helper layout, or a mixed factor fails compatibility before any keys are copied.
  4. Rejected affines. Independent fixtures introduce (0.01, 0.02, 0.01) scale, a shear term, a negative determinant, a zero/near-zero determinant, NaN/infinity, and one joint with an effective factor different from the common factor. Each must produce a stable typed rejection, no artifact, and no partial evidence publication. Two kinds of near-valid noise are treated differently, by design. Values that reach the tolerance-bearing affine classification — a 100.000015 IBM column, or a rest scale axis skewed within equal_axis — are accepted when their measured residual is inside the declared tolerance, and that residual is recorded. Structural facts checked at the §D.4 capability gate are compared exactly: a node matrix whose last row is not exactly (0, 0, 0, 1) is a NonAffineNodeMatrix refusal, so matrix[15] = 1.0000001 is rejected before any tolerance is consulted. The exactness is deliberate on three grounds: it matches glTF-Validator, whose isTrsDecomposable (lib/src/utils.dart) tests those same four components with exact != before it applies any tolerance and reports NODE_MATRIX_NON_TRS; composing two affine matrices produces an exactly zero bottom row in floating point (0*x + 0*y + 0*z + 1*0), so no real composition pipeline emits noise there; and the §D.2 rewrite rules rely on it — the reparameterization's node-matrix rebase writes components 3, 7, 11, 15 back as authored, so a tolerated matrix[3] = 1e-12 would be published unconverted where the basis change owed it 1 / s_i, and a tolerated matrix[15] = 1.0000001 would be rebased as though the node were affine. Relaxing it is a change to the §D.4 contract, not a tolerance widening; §D.1's ban on exact float equality binds the comparisons that tolerance policy governs — classification and proof — not this gate.

D.4 Modeled domains and preservation coverage

The transform plan owns an explicit capability manifest. A field absent from Document is not evidence that the source lacks that field; preflight must inspect the raw input or a loader-supplied complete inventory before mutation. The plan compiles every modeled container into a typed field ledger. Each row identifies the target and component, then states either a structural semantic rewrite kind or PreserveExact; absence is not used to mean preservation. A row does not carry a resolved factor, expected value, or connector product: candidate construction and proof independently derive those numbers from the operation and validated topology. PreserveExact is typed builder write-set ownership: the core builder does not write that field. It is not, by itself, a bit-equality rule for normalized derived state presented to prove_scale. Container rows carry the counts and shapes needed to describe every element without storing one row per vertex, key, tangent, or matrix component. Candidate construction consumes this ledger, so each authoritative modeled production write discharges one rewrite row and every rewrite row must be discharged. Whole-document conversion retains one compatibility-only exception: raw locals kept beside Unavailable source coverage are still converted best-effort, as before, but that coverage explicitly makes their identities non-authoritative, so they produce neither ledger rows nor proof/replay claims. Factor-one rows remain explicit where identity, aliasing, and write-set coverage depend on them. Separate typed payload-shape rows retain document, clip, track, instance, mesh, and primitive identity and cardinality, including empty containers, without freezing key times or other numeric values. Exact replay therefore detects added, removed, reordered, or retargeted payload that happens not to add a field row or obligation.

DomainCurrent model boundaryUnit conversionReparameterization
Rest hierarchyBone::rest; source-node TRS/matrix and parent topology form a canonical typed source tableScale translations; conjugate retained matrices; preserve topology explicitlyRebase affected normalized/projected locals; exactly preserve typed static-connector rows; bridge only the projected successor; preserve orientations and topology explicitly
Translation animationValues and cubic tangent triplets are retained for normalized nodesMultiply values and both tangents by qMultiply by the affected parent-basis factor; infer no raw animation support for an unprojected connector
Rotation and scale animationRetained as dimensionless valuesLeave unchangedRotation unchanged; rebase each scale VEC3 by s_parent / s_i, including cubic tangents
Root motion and velocityDerived from translation tracksConvert tracks, then recomputePreserve sampled trajectory and derived velocity
Base mesh geometryBase POSITION and normals are retainedScale positions; normals unchangedLeave positions and normals unchanged
MorphsNot retained by the normalized core; raw glTF ownership inventories target accessor identities and weight locationsRaw glTF only: scale dense f32 POSITION deltas once per unique accessor; preserve static JSON weights as numeric values and animated weight accessor payloads byte-exactly; reject other semantics and unsafe aliases/layoutsReject: rest/bind morph preservation is not proven
Skin bindsPer-instance IBMs plus a lossy bone convenience valueConjugate every per-skin matrixRegenerate every per-skin matrix from output joints and unchanged G
Cameras/lightsNode transform only; typed fields are not modeledReject until all length fields have handlersReject when attached to the affected domain until preservation is proven
Collision/custom dataNo semantic model for extras or extensionsReject unless a registered handler covers every lengthReject when affected unless exact preservation is proven
Other vertex/source dataSeveral attributes, non-triangle modes, and extension payloads are not writer-preservedReject on the normalized-model routeReject on the normalized-model route
Out-of-contract node transformsA matrix beside a TRS member, or a matrix whose last row is not exactly (0, 0, 0, 1) — compared exactly at the gate, never within tolerance (§D.3 case 4) — parses but is not glTF 2.0Reject: which transform the author meant is unknowable, and U M U^-1 leaves 3, 7, 11 unconverted where the basis change owes them 1 / q, while a 15 other than one is invariant under the rewrite and so would be published still asserting a projective divide (§D.2)Reject for the same reason, with 1 / s_i for 1 / q
Animation targeting a matrix nodeglTF 2.0 requires an animated node to use TRS; a typed reader can otherwise decompose matrix and lose that source distinctionReject at raw preflightReject at raw preflight
Shared raw accessor payloadsEvery accessor use has a component-shape and typed field disposition; preserved uses remain explicit factor-one claimsConvert once per unique accessor: every converted use takes qReject same-index type/factor conflicts and overlapping distinct accessors when either range is rewritten (§D.2)
Unreferenced accessor payloadsRetained only in the raw source container, including dense bases and sparse index/value payloadsPreserve exactly; reject when any owned byte span overlaps a rewritten accessorPreserve exactly; reject when any owned byte span overlaps a rewritten accessor
Image payload aliasesAn image reads a bufferView directly and never becomes an accessorReject when its bytes overlap a rewritten accessorReject when its bytes overlap a rewritten accessor

The narrow normalized FBX bridge applies the collision/custom row at the operation boundary rather than treating every parser-observed declaration as scale-bearing. User-defined properties are explicitly discarded before the private GLB is written, and parser-known texture/video linkage is represented by bounded resource facts and dependency-closure evidence rather than a transform field. The source-aware gate may therefore admit those two known classes while retaining their inventory/evidence rows; rooted capture keeps supported texture assignments in the normalized document and private GLB. The same proof boundary admits exactly ufbx's marker, LOD-group, stereo-camera, camera-switcher, and display-layer typed lists: those records supply no hierarchy transform, skin bind, animation track, or geometry to the normalized bridge. Display layers contain only node membership and editor visibility/freeze/color state. Shader and binding-table records are material-evaluation metadata and are admitted on the same basis. BindPose is different rest/bind-domain evidence: the same-load gate admits it only when it covers every joint of each skin it touches and every converted row is finite, unambiguous, and agrees under the fixed Appendix D scalar tolerance with the converted cluster bind or node rest-world matrix the bridge already consumes. A source without a Pose remains valid; incomplete, ambiguous, non-finite, or mismatching BindPoses fail closed as distinct residual kinds. All admitted rows remain counted in the raw unmodeled-element aggregate. Every residual unmodeled typed list fails closed, and its refusal retains the exact nonzero kind counts. This remains a normalized re-encoding rather than a raw FBX object-property, marker/camera/display-layer behavior, material, or texture-assignment preservation claim. Extensions, incomplete construct/resource coverage, and every unsupported transform, geometry, bind, or animation fact still reject. The inventory-only API remains conservative because its frozen v5 aggregate cannot distinguish these discharged classes from another unmodeled source element.

The connector exception is deliberately narrower than general helper-node support. The source projection can attest an unprojected row's identity, parent, and authored local rest, but not a raw animation channel targeting that row. A frontend that omits such a channel from the normalized model has not preserved it and cannot cite the static connector rule as evidence that it is safe to rewrite.

The current glTF writer rebuilds nodes as TRS, emits only modeled triangle attributes, creates skin/holder structures, and does not preserve arbitrary source JSON. The FBX loader mechanically inventories every current row above, including the unverifiable unreferenced-accessor analogue, and exposes a documented normalized ufbx source-node/source-skin sidecar when every declared joint slot is representable. A missing cluster bone downgrades the generic sidecar to unavailable, and one unreadable converted bind makes its whole ordered declaration unreadable with no retained prefix. Complete coverage here means every adjusted/compensated ufbx node and skin has stable identity and projected values; it does not relabel those values as raw authored FBX transform or payload evidence. The inventory also records that it bakes takes, normalizes coordinate and inheritance semantics, generates some missing data, truncates/renormalizes influences, counts rejected invalid or unrepresentable influences, triangulates, welds, and cannot prove raw payload spans. Consequently neither current load-Document-write route qualifies as a preservation-proof frontend for these operations without the raw capability preflight and explicitly bounded writer work. Unknown extensions or extras, unsupported morph semantics or unsafe morph storage, cameras, lights, collision metadata, non-triangle primitives, unmodeled vertex attributes, secondary influence payloads, malformed or missing inverse binds, node transforms outside the glTF 2.0 contract, and image payloads sharing bytes with a rewritten accessor fail closed. The FBX inventory is evidence of why the current frontend remains refused, not an enablement signal. A later FBX producer may enable only an operation whose complete inventory/write/proof boundary it satisfies; it must state that FBX curves were baked, not preserved as authored curves.

Source validity is decided once, at the preflight, and not re-derived per operation. The preflight's byte-disjointness inspection therefore ranges every accessor, whether or not a mesh, skin, or animation sampler references it, plus every image view. An unreferenced accessor contributes its dense base when it has one and the packed spans of its sparse indices and values; those bytes are preserved source rather than expendable payload. A referenced sparse accessor is still refused outright before disjointness matters, and extension-defined consumers are unreachable while every extension is itself a refusal. Every unreferenced span may sit adjacent to or alias other preserved data, but any overlap with a rewritten accessor refuses with both owners located. Each operation keeps its own guard as defence in depth, since the guard is what must hold if the gate is ever relaxed, but it shares the gate's classifier rather than re-deriving one.

The core typed ledger is also the semantic authority for both raw glTF scale writers. Before binding raw fields, the adapter replays the plan's complete structural inventory against the current normalized document; numeric-only replay remains valid, while a stale payload, field, topology, or obligation inventory refuses. One numeric-free glTF binding then projects canonical source topology and field dispositions onto raw node members, animation outputs, mesh payloads, and skin slots. The binding independently walks /nodes/*/children and refuses disagreement with canonical topology before writing bytes. Writer and artifact proof share those validated identities and component-group names, but independently select components and derive numeric multipliers; neither consumes the other's expected values, matrices, factor helpers, or domain model. The core ledger never replaces raw claims over scenes, materials, default scene, references, unknown payload, aliasing, byte ranges, or container bytes.

D.5 Separate-clip compatibility

Exact bone-name matching is necessary but insufficient. Before transforming or remapping a separately supplied clip, the producer compares a versioned skeleton-basis record containing:

  • named parent topology and target node paths;
  • finite local rest matrices, rest orientations, and helper-node layout;
  • declared/normalized coordinate convention;
  • effective uniform scale at every translation-track target;
  • selected reparameterization domain and factor; and
  • loader/tool identity plus the exact input-byte digest.

Compatibility requires topology and orientation agreement within their declared rules and the analytically expected translation-basis relationship. Different rest translation bases, parent scale, parent rotation, helper layout, or effective factor reject before remapping. A digest identifies the evidence record but does not replace tolerance-aware semantic comparison.

Character-assembly recipe/evidence v4 implements the base record and comparator; v5 composes it with canonicalization, grounding, and node removal. V6 admits the narrow normalized/baked FBX projection while leaving both older contracts immutable. It captures the exact base and clip bytes once, performs the format-specific capability preflight and plan before remapping, and rebases each compatible clip's translation values and both CUBICSPLINE translation tangents in the source basis. Evidence pins every input digest and basis fingerprint together with the compatibility result; v6 additionally binds the input container, projection kind, full FBX inventory, and private staged-GLB identity. V1 through v3 remain immutable and v3 rejects the v4 block as unknown. V7 generalizes the core compatibility basis around selector identity derived for each accepted plan and source document. Indexed mode retains v6's exact format-local indices, while named mode compares the exact resolved root and ordered selected-skin joint names plus semantic source/helper parent paths. Thus independently captured inputs may use different source-array indices without weakening topology, rest, orientation, or factor checks.

D.6 Proof, evidence, publication, and rollback

The candidate is serialized before it is proved, and the proof runs once, on the document reloaded from the exact artifact bytes — never on a separately built in-memory candidate, which would prove a document the rewriter did not emit. That single pass has two layers over the same reload: the normalized-document layer, which produces the residuals below, and the artifact layer, which re-derives directly from the container bytes every claim the normalized model cannot represent. Publication carries the proof to the file by digest, not by a second proof: the bytes are staged, read back, and refused unless their SHA-256 equals that of the proved bytes. A second proof would be redundant, the proof being deterministic over byte-identical input.

The artifact layer's claims are stated over raw source bytes — preservation of every raw JSON location and every buffer byte outside the rewritten set, array and index identity, honest reporting of the rewritten locations, container framing, bounds consistency, and single-step narrowing — and presuppose a rewriter that edits the source container in place. A frontend without one, which rebuilds the file from the normalized model as an FBX writer would, cannot make these claims and is not thereby exempt from the obligation they discharge: that declared-unaffected payloads are unchanged. Such a frontend owes that obligation by the other route — a complete loader-supplied inventory of every §D.4 domain — and may enable an operation only over the domains that inventory covers.

The exact raw-JSON-preservation refusal is diagnostic evidence as well as a refusal: the current scale-evidence v4 record carries up to the first 16 deterministic differences, each as a pointer and an added/removed/changed direction, plus the omitted remainder; the full count is the retained length plus that remainder. The field is null when the exact-preservation walk did not supply locations for a refusal; it is not an alternate spelling of the capability preflight's violations.

For the glTF producers, that raw complement explicitly includes scene membership, default-scene selection, materials, and their references: neither scale operation owns a write location in those payloads, so the artifact layer proves their raw JSON values exactly unchanged. The normalized core proof does not also deep-compare assets.scenes or assets.materials; those model values cannot replace the raw fields and references the container proof owns. A rebuilding frontend therefore cannot cite the core proof for those domains and must discharge them through the complete inventory route above before enabling an operation.

The proof evaluates rest, every key time, and analytic or sufficiently bounded interior times for cubic segments. For reparameterization it must prove, within versioned tolerances:

  • equal world joint translations and orientations before/after;
  • equal sampled animated trajectories, root motion, and derived velocities;
  • equal skinned vertex positions and bounds for analytic one-joint and multi-joint fixtures plus deterministic sampled production evidence;
  • unchanged mesh/material/skin identity and declared unaffected payloads — including that each candidate mesh instance draws the same mesh, binds the same skin joints, hangs off the same node, and names the same source node as its source counterpart, none of which either operation rewrites, and that a skin outside the affected closure keeps each slot's effective inverse bind. Identity here covers placement, not only what an instance is: the node is what positions an unskinned prop, and the source node is what resolves the skin whose absent bind accessor stands for the format-defined identity default. Both are compared positionally, so the instance list must also agree in length and order. Candidate proof also compares normalized parent topology and source-projection coverage exactly for every operation. When coverage is Complete, the raw-node parent/bone map is exact too; rows under Unavailable coverage are non-authoritative, produce no raw topology or field rows, and are not compared. Neither operation rewrites any authoritative identity. For rest/bind, the compiled ledger includes an ExactUnchangedWorldRest obligation for every bone in the exact unaffected complement and proof compares each complete composed world-rest affine exactly. The discriminating case is an independent sibling or leaf outside the closure's ancestry; an unaffected ancestor was already constrained transitively by an affected descendant's world-rest residual. This is an exact semantic placement invariant, not a tolerance or a claim that equal matrices prove every stored local field was retained; exact local ownership is a separate field-ledger obligation. Whole-document conversion has no unaffected complement, but its typed global topology obligation remains in force. Before either operation builds or proves, it recompiles the supplied source and requires exact equality of the operation variant, canonical source topology, payload and instance inventory, field dispositions, and derived obligation rows. Added, removed, reordered, or retargeted positional payloads (including stable-order source skins, attachments, tracks, meshes, instances, slots, or evidence) therefore make the plan stale even when the former aggregate boolean inventory would have been unchanged. Canonical source-node and connector topology is keyed by raw source identity, so merely reordering its backing rows is not a topology change. Numeric values may still change when that exact inventory and ledger remain equal; replay does not freeze the planned source snapshot. The shared input/candidate checks, source-skin structural rows, canonical source topology and rest/bind domain derivation, world-pose and inverse-bind readers, and one-time affected-skin classification live in crates/animsmith-core/src/scale/validation.rs. Operation selection, both planners, typed field/payload/obligation ledger compilation, clip-evidence declaration, and complete numeric-value-free replay live in private crates/animsmith-core/src/scale/planning.rs. Planning consumes the shared validation/topology boundary and remains the single owner of the compiled plan vocabulary; validation does not depend back on planning or reconstruct a second plan vocabulary. The opaque candidate wrapper, feature-gated analytic reference builder, and writer-owned direct/connector source-local rebase arithmetic live in private crates/animsmith-core/src/scale/reference.rs. That module consumes the compiled plan and shared validation/numeric leaves; proof derives its own connector products, expected source locals, and scale-track boundary rather than importing the reference writer's expectations. Those proof-owned derivations, exact field discharge, sampled work and budget accounting, semantic residuals, and skin/bounds checks live in private crates/animsmith-core/src/scale/proof.rs. Its paired residual recorder is nested under proof::residual, so only proof can mutate a residual maximum and its comparison count; the public read-only values continue to resolve through the animsmith_core::scale facade. For an admitted connector span, typed obligations require every connector local to remain bit-exact and independently derive the projected successor's expected bridged source local from canonical source topology. The connector owns no normalized residual, but a writer and proof cannot agree on a wrong connector rebase by sharing the writer's product calculation. Rewritten direct rewritten source-local components compare under the appendix-d-v6 scalar tolerance, rather than bit equality: a raw adapter may apply the same factor before its one f32 narrowing while the normalized source view was already narrowed. Preserved source-local components retain their exact-bit obligation. A connector-bridged projected successor also remains bit-exact: that core-only connector path does not cross a raw frontend narrowing boundary, and its exact connector-projection contract is unchanged. Operation-fixed rewrite and tolerance policy fields are not source inventory. Numeric affine/factor classification is not repeated there, so proof's normalized-skeleton witness remains independent of planning's raw-projection witness; a structurally stale plan still cannot omit a newly added bone, connector, or payload from every proof walk. The unaffected-bind comparison is over each slot's effective bind, in the resolution order the model defines: per-instance array, then the bone convenience value, then the format-defined identity only when a complete attached source skin declares its inverse-bind accessor Absent. When both sides resolve, the effective matrices are compared even if their stored representations differ. Exactly one side resolving is missing proof evidence; neither side resolving is reported as out of the proof's scope, never as proven — a document carrying an unrelated skin with no bind evidence and no licensed identity default is not an operation this record touches, and must not be refused for lacking evidence about it;
  • unit composed scale for every affected node, measured per axis against the derived bound of §D.1; and
  • the analytically expected full world affine of a transform-only attached child: preserved origin/orientation and unit linear scale, so a no-op cannot pass.

For whole-document conversion, the corresponding length facts must differ by exactly the declared factor within tolerance while dimensionless facts remain equal. The raw glTF artifact proof independently walks admitted morph POSITION accessors, requires one conversion per unique accessor (including base/morph and cross-target aliases), compares every static JSON morph-weight numeric value, and compares every animated weight accessor byte exactly. Both operations prove finite output, the skin equation, deterministic artifact bytes, and deterministic evidence bytes.

Every obligation above is derived from the evidence it needs. The typed ledger contains a row only when the validated planned inventory carries the payload that row reads — a transform-only attachment in the closure for a full-affine probe, an affected translation track for key-time checks, an affected cubic segment for interior samples, any affected track for sampled trajectories, a skinned instance touching the closure for one shared skinned evidence group, and a non-empty affected closure for the inseparable rest/unit-scale group. Skin-matrix and bounds results remain separately reported projections of that one skinned group; neither can be independently armed, and unit scale cannot exist without the affected-rest obligation. A row whose evidence disappears from a replayed source changes the recompiled ledger and yields typed PlanDocumentMismatch, never a zero residual. A counterpart or value missing inside an inventory-matched walk is typed MissingProofEvidence. The residuals are the evidence record, so a record stating "residual 0.0" for a claim nothing checked is false rather than missing. This matters most for the transform-only obligation, which is the claim ensuring a no-op cannot pass — a guarantee an empty probe loop reporting zero would not provide.

Per-element track values, base mesh positions, and inverse binds outside the affected closure are ordinary typed obligations rather than three special ungated loops. Their rows are derived for every corresponding planned container. For normalized Bone::rest, inverse binds, tracks, and meshes, proof discharges ownership through the existing versioned residual or exact semantic obligations, independently deriving rewritten expectations from the operation. A normalized candidate may therefore be non-bit-identical while remaining inside the declared tolerance; field-row ownership neither replaces nor tightens that accepted set. Rewritten and preserved track/mesh comparisons retain their released maxima and counts. Core bit equality applies only to authored SourceNodeRest locals that every admitted producer copies, including preserved connector locals. Exact structural obligations such as topology, connector projection, and unchanged world rest own no numeric residual. Raw byte-exact preservation of other fields — including signed zero and quaternion sign — remains the artifact proof's PR-B responsibility. No separately mutable boolean controls any of these walks.

Proof cost is bounded, not merely expected to be small. Source and candidate world matrices are derived once per sample time and shared across the trajectory, skin, and bounds obligations, and the skin and bounds obligations share one walk over the affected instances and their vertices. The remaining work is sample_time_count * per_sample_work_units with the per-sample cost spelled out in §D.1 — bones, skin slots, and skinned vertices, each charged once per document side — which the tolerance policy of §D.1 caps. A document above that cap is refused with a typed error naming the policy identity, the sample count, the per-sample cost, the computed work, and the budget. It is never proved against a truncated sample set: a bounded proof of a subset would publish evidence for claims it did not check. Because the budget lives in the policy identity, it is not a per-run flag, and the producer's fixed-policy rule below covers it.

The glTF/GLB producer emits immutable scale-evidence v4 containing the operation kind, declared and observed factors, tolerance policy and residuals, affected node/skin identities, raw capability manifest, input/output byte digests and counts, proof sample coverage/results, tool identity, and rejection reason. It is written as an atomic artifact/evidence publication pair. Failure leaves no new output; an existing pair is restored. The original input is retained so rollback is selecting the prior artifact, not attempting an inverse float rewrite.

Narrow FBX rest/bind path. With the ufbx feature, scale rest-bind may accept an .fbx input only when its complete inventory proves the normalized rest/bind subset: adjusted metre/Y-up coordinates, compensated inheritance, complete finite bind provenance, unchanged four-slot influences, and no unsupported scale-bearing semantic D.4 row. Same-load source facts may prove that user-defined properties, bounded texture/video declarations, or exact marker, LOD-group, stereo-camera, camera-switcher, and display-layer typed lists are known, non-scale-bearing inputs to this normalized bridge. Display layers contain only node membership and editor visibility/freeze/color state. Shader and binding-table metadata is equally non-bearing. BindPose rows are admitted only after the same-load converted matrices reconcile with the cluster/node matrices the bridge consumes; no Pose is required. Other unmodeled typed lists remain fail-closed and are reported by exact nonzero kind counts. The same-load boundary may also classify an exhaustive set of conversion-fidelity facts as scale-invariant: omitted authored vertex/face/edge metadata, influence truncation/rejection/renormalization with complete effective coverage, triangulation, and exact-bit welding. The frozen inventory continues to report those losses and its detached projection remains conservative. Missing effective influences, omitted point/line or mesh definitions, unsupported skinning/deformers, incomplete binds or normals, and unclassified payload stay fail-closed. The same-load FbxScaleSource retains the raw facts and dependency closure, while v5 keeps the frozen inventory projection rather than adding either sidecar to its wire shape. The three raw-span rows are deliberately unverifiable because FBX is not rewritten. The frontend serializes the normalized document to a private GLB, maps the selected root and ordered skin joint topology by exact unique normalized names, rewrites that GLB with the existing raw glTF writer, reloads and semantically proves the exact candidate once, read-back-digest checks it, and atomically publishes the resulting GLB with immutable scale-evidence v5. V5 records the complete FBX inventory and the private staged-GLB identity plus the nested raw-GLB proof; it makes no raw FBX span, object-property, authored-curve, material, or texture-assignment preservation claim, nor a marker/camera/display-layer behavior claim; supported linked texture bytes are nevertheless captured before staging so admission cannot silently remove them. Refusals name the exact incomplete coverage domain, semantic row, inventory counter, or residual unmodeled kind.

Two observed factors, and the divergence between them. For rest/bind reparameterization, "observed factor" names two independent witnesses of the same quantity, measured from deliberately different state: planning classifies the raw source projection's node-local rests composed through the raw parent chain, and proof reads the normalized skeleton's bone rests composed through its own parent chain. For whole-document conversion, both fields are the declared factor by definition and their divergence is exactly zero; that operation does not classify scale from either hierarchy. The raw composition includes every preserved static connector local. The normalized witness reads the corresponding folded transform through the next projected bone; the connector itself has no normalized identity and therefore owns no residual obligation. After semantic residuals succeed, proof's typed field-row discharge separately checks exact connector preservation and the analytic bridged source local at each projected successor. This preserves the released residual-error precedence while keeping both the connector's preserved authored components and its bridged projected successor bit-exact. Direct rewritten raw rows outside a connector bridge use the published scalar tolerance at the frontend narrowing boundary described in the compiled-ledger contract above. Strict scale-input shape validation requires the two chains to agree — under Complete source-skeleton coverage, which this operation requires anyway, a projection that contradicts its own skeleton is refused before either witness is taken — but nothing reconciles the two readings: each is still composed from its own stored state, and that independence is the reason both witnesses exist. The record therefore carries both, plus the relative divergence |planned - proved| / max(|planned|, |proved|) between them explicitly, so a consumer neither mistakes one witness for the other nor has to derive the relationship from two separate policy fields.

The expected ceiling on that divergence is the sum of two bands the policy already declares: the common-factor band plus the postcondition unit-scale residual (1e-5 + 2^-14 = 7.103515625e-5 under appendix-d-v6). Planning binds its witness to the declared factor within the first band or refuses; and for a candidate this operation built from the source under proof, that candidate's composed root scale is the proof witness divided by the declared factor, so the unit-scale postcondition binds the proof witness to the declared factor within the second. The two bands are not stated the same way, and the sum is a ceiling only up to that difference: planning's is relative to the max of its two operands, exactly as the divergence is, while the postcondition's is an absolute L∞ deviation from 1 on the candidate's composed scale — so it bounds |proved − declared| as a fraction of the declared factor rather than as a fraction of max(planned, proved).

The divergence is reported, not enforced. The second step above holds for a candidate this operation built from the source it is proved against — which proof deliberately does not require — and it costs the binary32 rounding of the rebase on the way, so the sum is the ceiling the design guarantees rather than a bound proved to the last ulp; refusing at it would refuse documents whose two witnesses each honour their own band. A divergence beyond it is not evidence that the two parent chains disagree — those are validated to agree, above — but that the state each witness reads differs: most often the raw node-local rests against the normalized bone rests, two separately stored descriptions of one rest pose. How far apart those are is a fact about the input, not a residual this proof owns. Recording both witnesses and their divergence introduces no band of its own and no new policy identity.

Migration is opt-in. assemble.canonicalize_skin in recipe v3 remains the existing unanimated bind-geometry operation with identity source-to-metre conversion; it does not gain rest-scale rewriting, and old recipes are not silently reinterpreted. Recipe/evidence v4 adds the optional, explicit rest_bind_scale operation and basis-compatibility evidence. Consumers that invert scaled inverse binds with the rigid-only shortcut -A^T t can observe a change by s^2; that exposes a pre-existing consumer error because the shortcut is valid only for an orthonormal linear part, not a geometry change made by this operation.

D.7 CLI, configuration, and public API

The CLI uses a required operation subcommand, not one flag whose meaning depends on the input:

animsmith scale whole-document INPUT.glb -o OUTPUT.glb \
  --factor 0.01 --evidence OUTPUT.scale.json

animsmith scale rest-bind INPUT.glb -o OUTPUT.glb \
  --source-skin-index 0 --source-root-node-index 3 \
  --expected-factor 0.01 --evidence OUTPUT.scale.json

All numeric and source-identity arguments shown are required; there is no inferred factor, implicit first skin/root, or in-place mode. Artifact and evidence paths must be distinct from each other and the input. The narrow FBX path additionally refuses either destination when it names a retained safe dependency key, and refuses publication entirely when that key was rejected as symlink-mediated or the dependency closure stopped before enumerating every key. Initial support is self-contained glTF/GLB for both operations, plus the narrow inventory-complete FBX rest-bind path that emits a .glb and v5 evidence. The command uses the fixed tolerance-policy version recorded in evidence rather than accepting per-run tolerance flags. A future policy change requires a new policy identity and compatibility review.

The single-document producer has no animsmith.toml key and no separate plan file: mutation must not become an incidental effect of a lint configuration. Assembly recipe v7 exposes the same rest/bind operation through an optional [rest_bind_scale] block whose exact root_node_name and factor are required. The name rejects leading or trailing whitespace instead of normalizing it. For every captured base and clip input, that name must resolve to exactly one normalized source node and exactly one non-empty source skin whose every joint is that node or its descendant. The resolved per-input source indices feed the unchanged core operation and private staging bridge; no cross-file source-array index is inferred or reused. It accepts glTF/GLB plus the narrow FBX inventory admitted by the standalone rest/bind boundary, validates every base and clip basis before applying the operation or remapping keys, and records the declared name, resolved per-input name/indices, and normalized/baked FBX projection rather than claiming raw FBX curve or span preservation. Recipe v6 remains immutable with its explicit source indices, recipe v5 remains glTF-only, and recipe v3 has no such block and continues to reject it as unknown.

The public animsmith-core shape is a non-exhaustive ScaleOperation with distinct WholeDocumentLinearUnits { factor } and RestBindUniformScale { source_skin_index, source_root_node_index, expected_factor } variants, carried by a ScaleRequest. Pure planning returns either an opaque, non-exhaustive ScalePlan (operation variant, affected closure, canonical topology, typed field rows, tolerance policy, and derived obligations) or a typed ScaleError; proof returns a ScaleProof with one read-only ScaleProofResidual per semantic claim. Each value carries the maximum and number of comparisons together, so a residual nothing walked is distinguishable from a measured zero (issue #319) and an adapter cannot combine one claim's count with another claim's maximum (issue #323). Planning and proof take format-neutral node, track, bind, and capability facts. They do not accept paths, glTF/ufbx types, config parsers, or publication policy. The format frontend owns raw inventory and exact source rewriting; the CLI owns atomic artifact/evidence publication. Applying a plan builds a candidate and never mutates the caller's source on failure.

animsmith-core::scale implements this contract (issue #281): the ScaleOperation/ScaleRequest planning entry point, the typed ScalePlan and ScaleError, the no-authority candidate handoff, and paired ScaleProof residual values. Any change to the operation variants, required selectors, or failure boundary is a design change rather than an implementation detail.

ScalePlan exposes its compiled ledger only through non-exhaustive read-only field, payload-shape, topology, and obligation row views; callers cannot use those row values to construct or mutate a plan with an incoherent operation, topology, field inventory, or obligation set. Plan equality includes that exact structural ledger. The former public ScaleDomainRewrites and ScaleProofObligations boolean bags and their plan accessors are removed in this pre-1.0 API break: operation-fixed facts and evidence gates have one typed authority rather than independently selectable booleans. The CLI privately projects the same five domain_rewrites booleans from the operation variant's frozen mapping, so payload presence and factor-one rows cannot change their meaning. Immutable scale-evidence v1/v2/v3, ScaleProof serialization, appendix-d-v6, tolerances, calibration semantics, and policy pins do not change. The internal ledger is not serialized into evidence.

ScalePlan and ScaleProof each carry the observed factor beside the declared one, as §D.6's evidence contract requires (issue #290). The declared factor is what the build applies and what every proof expectation is stated in; the observed factor is the rest-world uniform factor measured at the scaled root, which the declared-factor band admits without requiring equality. Proof re-measures it from the source it is handed rather than reading it off the plan, so evidence does not depend on planning having recorded it. It is reported, not checked: the input band is planning's obligation and the postcondition derived from it in §D.1 is proof's. For a whole-document conversion the observed factor is the declared one, because §D.1 gives that operation's factor no measurable source counterpart at all.

animsmith-gltf owns the glTF/GLB frontend of that contract: preflight_scale_source (issue #280) captures the raw inventory, capability_facts / rewrite_linear_units / prove_rewritten_artifact (issue #282) implement the whole-document conversion of §D.2 directly on the source's own JSON and buffer bytes, and rewrite_rest_bind / prove_rewritten_rest_bind (issue #283) implement the rest/bind reparameterization the same way. Because that route produces a candidate core did not build, ScaleCandidate::from_document exists so the reloaded artifact can reach prove_scale; the type asserts nothing that prove_scale does not independently re-derive. Core exposes no production candidate builder: its analytic reference constructor is available only through the non-default fixtures feature for tests and calibration.

rewrite_rest_bind is the selector-taking convenience boundary; it compiles a plan and delegates to the same plan-taking raw adapter used by rewrite_scale_plan. The latter validates the plan against the supplied source before any write, so the CLI can carry one authoritative compiled plan through rewrite and proof without admitting stale-plan replay. GltfScaleArtifact reports which operation produced it beside the declared factor, because the two operations rewrite different domains and a factor alone does not distinguish them. Beyond the shared plan/proof rejections, the frontend owns three refusals the format-neutral layer cannot state: the disagreeing-multiplier refusal of §D.2, and two agreement checks over the raw node hierarchy, the loader's source-node projection, and the normalized skeleton's parent links. animsmith-core requires the last two of those three to describe the same tree — that is the document-shape validation of §D.6 — but the raw /nodes/*/children arrays live in JSON that never becomes a Document field, so bringing the third description into the comparison is only possible here.

D.8 Implementation ownership and deferred integrations

Putting all semantics in assembly was rejected: it would hide a reusable single-document operation and preserve assembly's currently insufficient separate-clip proof. A standalone command with unrelated duplicate math was also rejected. The chosen end state is one shared core transform-plan and proof layer with distinct, explicit frontends: the shipped single-document producer and character-assembly recipe/evidence v7 integration. The existing canonicalize_skinned_bind_pose remains the narrower unanimated bind-geometry foundation; it is not silently widened or renamed into this contract.

Shared affine classification, distinct acceptance contracts. The core uses one typed, symmetric f64 positive-uniform Mat3 classifier for both Appendix D and skinned bind-pose canonicalization. Appendix D retains appendix-d-v6's 1e-5 equal-axis/orthogonality and 1e-6 relative-determinant thresholds. The canonicalization input source_to_meters_y_up retains its numeric 1e-4 equal-axis/orthogonality thresholds and exact-zero determinant threshold; these are compatibility with the existing assembly/canonicalization contract, not a second scale-policy version.

Canonicalization does deliberately adopt the shared classifier's semantics: axis equality is relative to the three-axis average and longer operand rather than to the first column, derived arithmetic is f64, and singularity is classified before non-uniformity or shear. Thus a unit basis with one axis at 1.00015 is accepted while 1.00016 is refused, and an input that is both exactly singular and sheared now uses the existing reflection_or_singular reason rather than non_uniform_or_sheared. The machine-readable vocabulary is unchanged; these symmetric boundaries and precedence replace two independent algorithms with one explicit typed policy. The classifier runs after canonicalization's separate existing zero-translation/fourth-column w_axis ~= Vec4::W gate and its first-axis <= 1e-4 zero_scale reason. That zero-scale gate reads the same widened f64 axis length as the classifier, so a finite large basis cannot overflow a binary32 length intermediate and be mislabeled as zero scale. Near the shear boundary, the symmetric average base and binary64 dot products can also refuse inputs that the former first-column/binary32 check accepted.

Measurement shares facts, not the positive-only verdict. Measurements v12 derives axis lengths, their canonical ascending-order mean, determinant, axis-length product, and XY/XZ/YZ dot products from the same crate-private widened f64 fact record. It also uses the shared symmetric mean/longer-operand equal-axis predicate, which removes v11's privileged-X result and reconciles the finite diag(1, 1, 1.000012) fixture. Measurement still asks a different question: it keeps finite partial evidence for singular, reflected, sheared, and non-uniform inputs; normalizes each orthogonality comparison by that axis pair's length product; and classifies in singular, reflected, sheared, unit/uniform, then non-uniform precedence. Calling the positive-uniform classifier directly would discard those descriptive facts and impose the operation's different precedence. Skin-bind summary factors are sorted before their mean is derived and every factor is compared to that mean, so authored joint order cannot choose consistent_uniform versus mixed_uniform or the reported common factor. These corrected numeric semantics require the nested measurements-v12 identity. The JSON shape remains unchanged, but the outer identity advances to output-v6 because each immutable output schema statically pins its nested measurement URN; measurements v11 and output-v5 remain immutable historical evidence. In general, changing a nested identity that an immutable outer schema references requires a new outer identity too; historical outer schemas are never retargeted or broadened in place.

Measurements v13 makes inverse-bind derivation auditable and fail-closed. Each per-joint bind observation retains the exact finite raw inverse-bind matrix for its accessor slot. Before publishing an inverse, the source bottom row must be within absolute 1e-6 of affine [0, 0, 0, 1]; this admits several binary32 round trips around one but refuses projective translation columns. The linear 3x3 reports the scale-free reciprocal infinity-norm condition number 1 / (norm_inf(A) * norm_inf(inverse(A))). Exactly singular sources and finite sources at or below 1e-6 use distinct non_invertible and ill_conditioned unavailable reasons. This avoids the prior scale-bearing, exact-zero determinant decision: a uniformly small but well-conditioned matrix is assessed by its error amplification rather than its determinant magnitude. The new nested shape advances the immutable outer envelope to output v7; measurements v12 and output v6 remain historical.

Measurements v14 gives every clip fact that is not applicable to every clip — loop continuity, loop endpoint mode, frame grid, loop seam ratio, gait (and its own phase), and root-motion speed — a required sibling <field>_availability status of measured, not_applicable, or unavailable. A bare optional value cannot distinguish "this clip has no subject for the fact" (not_applicable, a legitimate absence) from "the subject applies, but derivation failed" (unavailable, which a consumer must fail closed on rather than silently treat as passing); those two absences now compare distinctly under diff as well. The value field is present if and only if its status is measured. The new nested shape advances the immutable outer envelope to output v8; measurements v13 and output v7 remain historical.

Measurements v15 inventories each clip's surviving non-empty local TRS channel set by skeleton index and adds normalized model-space root-trajectory regression evidence. A resolved Root always wins; Hips is a typed fallback only when Root is unresolved. The record retains independent translation and yaw availability, signed endpoint displacement, sampled horizontal travel and vertical extrema, and fixed-witness net/unwrapped/travel yaw. All trajectory path/extrema/winding values come from the shared inclusive uniform metric grid and therefore do not prove continuous-curve extrema or authored transform preservation. The new nested shape advances the immutable outer envelope to output v9; measurements v14 and output v8 remain historical.

The implementation status is:

  • shipped: raw glTF/GLB capability preflight, the shared core plan/ledger and proof, preservation-safe whole-document and rest/bind exact-source writers, artifact proof, and atomic CLI publication with scale-evidence v4;
  • shipped: issue #286's conservative FBX rest/bind re-encode path with the full ufbx inventory, exact staged-GLB rewrite/reload proof, v5 evidence, and atomic GLB/evidence publication; whole-document FBX scaling remains refused;
  • shipped: glTF/GLB and inventory-complete normalized/baked FBX assembly integration in recipe/evidence v6, including the versioned basis fingerprint/comparator, pre-remap clip rebasing, exact input digests, compatibility evidence, and proof over the exact staged artifact;
  • shipped: recipe/evidence v7 exact-name rest/bind selection, with a unique source-node and fully-governed-skin resolution for every captured input and explicit declared/resolved selector evidence (issue #459);
  • shipped: issue #286-A's explicit conservative ufbx-side status for every current D.4 domain and documented normalized source-skeleton projection;
  • shipped: raw glTF whole-document scaling for dense f32 morph POSITION deltas with numeric-value preservation for static JSON weights and byte-exact preservation for animated weight accessors (issue #298); rest/bind morphs and a normalized core morph model remain out of scope.

The live implementation sequence is owned by tracker issue #344 and roadmap issue #165, not by a numbered future-slice list in this decision record. Work beyond this positive-uniform, fail-closed decision remains ADR-gated.

Appendix E — decision record: engine profiles

This appendix is the accepted design outcome of issue #150. It defines the consumer-model boundary required by the 0.4.0 engine-feedback milestone; it does not claim that the configuration, checks, or generators described here already ship.

E.1 Decision and ownership

An engine profile is a versioned set of externally verified importer or runtime facts. It answers how a named consumer handles a supported input. It is not any of the following:

  • a rig profile, which resolves semantic roles to bones;
  • project intent, such as whether a clip should be in-place, looping, or animation-driven root motion;
  • a severity or artistic-quality preset;
  • evidence that a particular engine import actually ran; or
  • authority to rewrite an asset.

Those distinctions are structural. [rig] continues to own role resolution, [runtime_nodes] owns the shared selection of project-declared attachment, socket, IK, collision, and other runtime-facing nodes, [clips] owns per-clip intent plus explicitly nested clip-scoped importer choices, [checks] owns activation, severity, and project tolerances, engine readback owns observed import results, and Appendix D plus each transform's own proof contract owns rewriting. An engine profile may describe a hard importer constraint or interpretation, but it must not silently change the math, thresholds, or findings of an existing consumer-neutral check.

The issue's original suggestion that profiles own loop/contact thresholds is therefore rejected. An engine may expose a loop toggle or a compression setting; it does not know a project's acceptable seam, contact, or slide error. Generated guidance may propose a documented starting value, but the effective value remains explicit project policy.

E.2 Selection and built-ins

The first configuration surface is deliberately small:

[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_*"]
movement_owner_xz = "animation"
movement_owner_y = "gameplay"
movement_owner_yaw = "animation"

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

[clips.idle]
movement_owner_xz = "gameplay"
movement_owner_y = "gameplay"
movement_owner_yaw = "gameplay"

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

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

[runtime_nodes].selectors is project policy, not an importer fact. Exact names and * globs resolve with the deterministic miss and ambiguity behavior already established by issue #268. The existing [checks.rest-world-scale].node_selectors field remains accepted as a compatibility alias and is normalized into this shared selection before evaluation. Declaring both forms is a typed configuration error rather than a precedence rule. rest-world-scale and the selected-node facet of engine-unit-scale consume the same resolved set; one check never reads the other check's private settings. An absent selector field and an explicit empty selector list both mean no declared policy, preserving the legacy check's behavior. Only that state makes the selected-node engine facet genuinely not applicable. If a declared selector misses or is ambiguous, rest-world-scale retains its existing engine-neutral coverage behavior, while an enabled, otherwise applicable engine-unit-scale facet emits required_prediction_unavailable and follows E.4's exit policy. Component/root and mesh-node unit-mapping rules may still apply independently.

An absent [engine] section means today's engine-neutral behavior. There is no generic engine profile, no auto-detection, no nearest-version match, and no fallback profile. Family, profile revision, engine version, and importer route are all required when [engine] is present. An unknown or unsupported tuple, unknown setting, or setting that is invalid for that importer is a typed configuration error. A caller that still wants ordinary engine-neutral linting removes [engine]; it does not receive a partial prediction under a different consumer contract.

Every profile setting descriptor declares its exact value domain, applicability, and either document or clip scope. Document-scoped importer choices live only under [engine.settings]. Clip-scoped importer choices live only under the matching [clips.<selector>.engine_settings] table and resolve through the existing clip-selector rule (exact name over matching globs, later matching globs winning ties). For the Unity Generic FBX profile, the Motion/Root Motion Node choice is an exact source-transform path that applies to all imported clips and is therefore document-scoped as root_motion_source. It is explicitly not applicable to the Unity Humanoid profile, whose importer uses Avatar/body semantics. Rotation and position bake/extract choices remain clip-scoped for both Unity profiles. [clips.<selector>] movement-owner fields remain project intent; the sibling engine_settings table records the importer state that a prediction must compare with that intent. A setting at the wrong scope is a typed configuration error, not an override.

Each applicable V1 setting is classified as required without a verified default; values in the example are not defaults. Adding a documented default requires a later profile revision and settings-contract revision rather than a silent registry edit. Omitting a required setting is a typed configuration error; it is never guessed, treated as an implementation default, or deferred to a coverage gap. The resolved settings record therefore contains one exact document value or per-clip value for every applicable setting.

The registry contains the preserved revision-1 tuples plus the current revision-2 Unity Generic, Godot, Unreal, and Bevy tuples and the revision-3 Bevy tuple:

familyrevisionengine versionimporterAnimSmith input boundary
unity-generic26000.3fbx-model-importerFBX
unity-humanoid16000.3fbx-model-importerFBX
unreal15.8fbx-importerFBX
unreal25.8fbx-importerFBX
godot14.7resource-importer-sceneglTF JSON, GLB, FBX
godot24.7resource-importer-sceneglTF JSON, GLB
bevy10.19.0gltf-asset-loaderglTF JSON, GLB
bevy20.19.0gltf-asset-loaderglTF JSON, GLB
bevy30.19.0gltf-asset-loaderglTF JSON, GLB

These are exact versions, not ranges. The Godot row is AnimSmith's bounded V1 profile boundary rather than a claim that Godot cannot import other formats. The preserved Unity revision-1 advice profiles expose document booleans convert_units and bake_axis_conversion, plus per-clip root_rotation, root_position_y, and root_position_xz values in the closed bake | extract domain. Current Unity Generic revision 2 instead exposes the frozen document values animation_type = "generic", avatar_setup = "create_from_this_model", import_animation = true, and the exact document-scoped root_motion_source, plus those three per-clip root controls. Only Generic also exposes root_motion_source; it is not applicable to Humanoid. Unreal, Godot, and Bevy revision 1 expose no V1 setting vocabulary, so every supplied key is rejected. Bevy revision 2 has a separate closed setting vocabulary for exact loader coordinate toggles, mesh loading, extension-handler environment, animation feature, and animation loading. It materializes version-pinned defaults and retains each value's explicit/default origin.

Bevy revision 3 preserves the earlier tuples and adds only the narrow animation/channel gate-support fact. It resolves the compiled bevy_animation feature and load_animations with explicit/default origins; the compiled feature gate takes precedence. Its raw same-load inventory binds animation coverage independently from each animation's channel coverage. Extensions, other animation constructs, and positive runtime survival remain outside this revision; the latter requires an actual Bevy readback. The separately versioned bevy-gltf-addressability-rules:1 bundle paired with this immutable profile owns the richer scene, skin, named-map, and target-path projection; it does not mutate profile revisions 1--3.

The two Unity ids are import modes, not skeleton-name maps. Likewise, unreal, godot, and bevy do not imply a mannequin, humanoid bone map, or other rig profile. glTF is not an engine profile: its specification defines format semantics but deliberately does not define looping, playback, import options, or other runtime policy. glTF 2.0 compatibility remains a loader and consumer-neutral check boundary. Bevy has separate supported-extension, asset-addressability, and animation-target behavior.

Each built-in resolves its exact (family, profile_revision, engine_version, importer) tuple to an immutable fact-bundle identity such as urn:animsmith:engine-profile:unity-generic:2. Its data record contains exact engine version and importer values, accepted input formats, source and target coordinate bases, importer/runtime facts, allowed setting vocabulary, and primary-source references with a verification date. For every claimed input format it must also enumerate, rather than leave implicit:

  • handedness, up/forward axes, and the selected importer's axis-conversion mapping;
  • each applicable clip-boundary predicate (including whole-frame constraints) or an explicit statement that none is known; and
  • supported, dropped, and reinterpreted channels, extensions, and animation constructs, with an explicit unsupported or unknown state where evidence is incomplete.

Changing any such fact requires a new profile revision and normal release notes; a historical result never silently resolves through the current revision. [engine.settings] selects only choices the immutable importer facts already expose; it cannot override those facts.

Facts use explicit known, unknown, and genuinely not-applicable states. The accepted-format fact is the sole input-format authority. The facts digest is the core InputIdentity of a versioned, fixed-field-order, length-prefixed UTF-8 encoding of the full tuple, URN, every fact state and value (including accepted formats), every setting descriptor/domain/scope/applicability and default status, verification metadata, and source references sorted by stable id. Source references bind stable fact/setting ids to one target version, URL, and verification date; live documentation bytes are never digest input. The settings digest uses the same encoding over the full resolved identity and fully materialized document settings plus actual clip names/settings in lexical order, never TOML spelling, selector patterns, or map insertion order.

Version one does not accept arbitrary engine-fact overrides in TOML. A user cannot safely make an importer support a channel or reinterpret root motion by assertion. Embedders may supply namespaced custom checks and keep their own consumer contracts, as they do today, without extending the built-in profile registry.

Crate ownership is fixed before implementation. A new format-neutral animsmith-engine library owns the built-in fact registry, strict tuple resolution, prediction-rule adapters, and the standalone advisory/readback payloads. It depends on animsmith-core, but not on format libraries, TOML, a filesystem, or engine SDKs. animsmith-core continues to own consumer-neutral documents, measurements, checks, shared source-evidence value types, every measure/lint envelope and URN, and a registry-independent wire projection for profile identity, rule basis, prediction state, and shared provenance. The engine library populates that core-owned projection; core never depends on the engine registry or its concrete profile types. The glTF and FBX libraries populate the common raw-source projection while retaining their format-specific parsing details. The animsmith CLI owns strict TOML mapping and orchestration; animsmith-report renders the resulting public contracts. This preserves Appendix C's envelope ownership, prevents a core/engine dependency cycle, and gives embedders the same registry and resolver as the CLI.

Format loaders bind that projection to the normalized document in an opaque, immutable LoadedSource. Its consumer surface is deliberately limited to document(&self), source_facts(&self), dependency_closure(&self), and into_document(self): callers cannot mutate a document while continuing to present its source facts or captured closure as current, and consuming the document explicitly discards both sidecars. The facts view borrows the canonical Document.assets.source_skeleton projection rather than copying source-node or skin authority. Existing document-only loader entry points remain compatibility conveniences that consume this wrapper.

The V1 projection is bound to the exact primary-file InputIdentity and an explicit source format established by the loader; SourceInfo.path and its free-form format label are neither identity nor fact authority. Row sets carry independent complete, partial, or unavailable coverage. Rows retained under partial coverage prove positive presence only; absence is established only by complete coverage. Observation availability, provenance, and AnimSmith loader disposition remain orthogonal and never imply target-engine support.

V1 bounds projection work to 65,536 enumerable rows, 4,096 clips/takes, 4,096 resource-reference declarations, 4,096 UTF-8 bytes per retained source string, 8 MiB of retained source strings in total, and traversal depth 128. At N+1 the loader keeps the deterministic source-order prefix (lexical order only where a parser exposes a map without source order), marks the affected domain partial with projection_budget_exceeded, and stops new work in that domain. A legacy load that succeeds does not become a failure merely because this sidecar is partial. These are projection limits, not claims that every existing parser, resource read, image payload, or bake allocation is globally bounded.

Raw resource facts stop at bounded declaration identity, kind, and a safely retained source-relative spelling. They do not themselves open, normalize, deduplicate, hash, or map referenced content. Unsafe absolute, remote, data-payload, or escaping spellings are classified without being reproduced in the new facts surface, its debug/serialization views, or its errors. Legacy diagnostic Document::source.path remains outside that projection.

The separate dependency-closure v1 sidecar is exact only over that versioned declaration domain. Format loaders construct it during the same load that consumes the resource bytes; a post-load reopen cannot establish the identity of bytes already parsed. Embedded/data/BIN/view-backed declarations map to the primary identity. Safe relative declarations map through a normalized source-relative key to one captured InputIdentity; aliases of one key are opened and hashed once, while different keys remain distinct even when their bytes match. The source-order reference map and key-sorted distinct-resource table form a domain-separated canonical closure identity. Only complete raw declaration coverage with an identity for every declaration produces a complete closure identity. Unsupported extensions or unmodelled FBX resource-bearing domains keep the result conservative rather than turning a known subset into a whole-document or target-importer claim.

Each reference also records a purpose derived from its source kind: loader-essential buffers, nonessential images/textures, or target-only video/cache declarations. This is source/loader evidence, not a prediction of whether a selected engine profile will import the resource.

Closure capture has its own immutable v1 budget: at most 4,096 declarations, 1,024 distinct external keys, 4,096 UTF-8 bytes for each source spelling and normalized key, 128 path components per spelling, 8 MiB of aggregate normalization input, 64 MiB read and hashed per external resource, 256 MiB in aggregate, and one bounded non-quadratic deduplication probe per declaration. The loader checks N+1 before allocation or opening, retains the deterministic prefix and work counters, and stops the affected capture. Absolute, escaping, out-of-root, remote, malformed, oversized, and symlink-mediated locators are typed refusals and are never opened or reproduced. A trusted resource root comes from path-based loading or an explicit byte-loader API; byte loading never falls back to the process working directory. Missing loader-essential bytes remain a load/operator error, while unavailable nonessential or target-only bytes may leave typed partial closure. No captured resource is recursively parsed for more declarations. The limits bound this closure walk; they do not certify the primary parser or a concurrently mutating trusted filesystem.

E.3 Rules, checks, and precedence

Engine rules consume engine-neutral measurements plus a dedicated read-only raw-source-facts projection; they do not recompute those facts differently per engine. The normalized Document alone is insufficient because importer- sensitive extensions, authored FBX curves and units, take boundaries, and other declarations may already have been normalized or discarded. The raw projection preserves such facts and their availability without exposing a scale operation's private capability ledger as the general engine API.

That projection is the one shared provenance-and-coverage model for raw source evidence, not a parallel per-feature copy. Loaders reuse its identity, availability, and provenance primitives wherever scale planning needs the same source observation. A scale capability inventory and proof ledger remain operation-specific derived consumers because they additionally encode a requested rewrite's closure and proof obligations; engine rules do not create a third representation of the underlying source fact.

The format-neutral core also owns urn:animsmith:exact-source-timing:1, a bounded same-load sidecar attached to a loaded source. It records generic exact time units, frame periods, selected clip ranges, and typed availability without format names, parser types, or format constants. A format adapter performs its own projection into that vocabulary: the FBX adapter alone owns KTime constants, integer period arithmetic, and ufbx resolved-property selection. Engine rules consume the generic sidecar through RawSourceFactsV2 and prediction provenance v3; neither the V2 outer identity nor the historical readers are widened.

A rule declares all of its preconditions: the full resolved profile identity, input format and raw-source coverage, required measurement fields, rig roles where applicable, and every explicit project intent or importer setting. A required engine-prediction facet with missing evidence records scoped required_prediction_unavailable rather than a passing finding, guessed value, or generic engine-neutral coverage gap.

New engine prediction checks are grouped by stable concern rather than copied once per engine. The initial families are:

  • engine-clip-boundary for ranges, whole-frame requirements, and slicing;
  • engine-root-motion for importer extraction/bake interpretation;
  • engine-track-support for channels, extensions, and data an importer drops or reinterprets;
  • engine-unit-scale for explicit unit conversion and resulting transform scale; and
  • engine-addressability for scenes, animations, targets, and runtime labels.

The current engine-root-motion slice is the exact Unity Generic revision-2 / 6000.3 / fbx-model-importer FBX tuple. Its V6 provenance binds the V5 same-load source evidence to a bounded raw transform-path inventory and the normalized per-clip movement-owner intent. The rule is applicable only when that exact profile is resolved and at least one axis is explicitly declared. For each declared horizontal_xz, vertical_y, or yaw axis, it compares movement_owner_xz, movement_owner_y, or movement_owner_yaw with root_position_xz, root_position_y, or root_rotation. bake is baked_into_pose; extract is stored_as_root_motion. Gameplay ownership matches baking, animation ownership matches extraction, and a mismatch emits an ordinary error finding bound to the available prediction facet.

The rule has one explicit lifecycle: all available axis facets are complete, a mixture of available and required-unavailable facets is partial, and all required-unavailable work is not_evaluated. Missing or ambiguous path resolution, a path that does not identify the explicitly resolved Root role, incomplete raw-path or project-intent coverage, settings overflow, duplicate clip names, or unavailable axis-specific translation/yaw evidence emits required_prediction_unavailable; this state is not a content finding and cannot be suppressed by --allow. The rule never falls back to Hips, even though the consumer-neutral root-trajectory measurement still uses Hips when Root is unresolved. Measurement availability is required, but numeric translation/yaw magnitude is not a threshold and does not affect routing.

Generic's root_motion_source is a closed, case-sensitive, byte-exact relative path. / is the only separator and has no escape syntax. Empty segments, leading/trailing or doubled separators, ./.., backslashes, Unicode control or format characters are rejected. The bounds are 1,024 UTF-8 bytes per segment, 4,096 bytes per path, and 256 segments. The FBX adapter projects source identities, names, and parent chains from the same input bytes with a raw-preserving ufbx load. The implicit ufbx root and generated geometry/scale helpers remain rows but cannot match. Complete inventory coverage is required to prove NoMatch; partial or unavailable coverage yields CoverageIncomplete. This is source evidence only: AnimSmith does not execute Unity, read back an imported asset, play runtime clips, or certify engine behavior. Repository and CI fixtures for this slice are self-authored synthetic FBX files only.

The selected profile chooses applicable rules within those check ids. The file-scoped provenance retains the full resolved profile and source identity; each check retains prediction facets keyed by the existing evaluation scope, with the exact fact, setting, policy, and measurement basis for clean and finding-bearing work alike. Findings bind to an available facet rather than introducing another applied-rule id. Checks do not use unity-*, unreal-*, and similar duplicate families.

The bounded 0.4.0 production slice is the exact Bevy revision 1 / 0.19.0 / gltf-asset-loader animation-index selector rule. With complete nonempty glTF or GLB source-animation inventory, engine-addressability emits one available facet per source row whose subject is Bevy's Animation{source_clip_index} display label. A source name is only metadata: absent and duplicate names do not change the index selector. Partial or unavailable inventory emits one required-unavailable inventory facet and no retained-prefix predictions, so the 4,096-row raw-source and file-facet caps compose without an N+1 facet. The resolved-settings/provenance v1 contract separately caps actual clip rows at 4,096. A 4,097-clip document is therefore a bounded operator error before prediction, not a silently truncated settings inventory; #485 owns any future overflow evidence and shared multi-rule facet-budget policy. This predicts the canonical selector convention only. It does not claim a successful Bevy load, runtime asset existence, animation-target survival, or graph wiring, and the index is not stable across source-order edits. Richer scene/name/target addressability remains a separate inventory/adapter concern.

The current additional production slice is the exact Unreal revision 1 / 5.8 / fbx-importer whole-end-frame rule. For every retained FBX animation stack, the FBX adapter projects ufbx's selected complete range pair and exact KTime period into the generic exact-source-timing sidecar. engine-clip-boundary tests only whether the absolute selected end coordinate is on that lattice. Incomplete inventory and unavailable declarations or ranges yield scoped required-unavailable facets; decimal FPS, inferred frame numbers, import-range selection, resampling, compression, root motion, and all other importer policy remain out of scope.

The corresponding 0.4.0 standalone producer is generate addressability. Its V1 root is separate from output-v11 and contains one engine-neutral glTF/GLB animation inventory plus a nullable exact-Bevy adapter. animsmith-engine owns this format/profile-specific envelope and its strict reader; core remains limited to the generic source, closure, prediction, and evaluation contracts it consumes. The neutral identity covers the primary input, full existing dependency closure, and bounded raw animation/channel observations only; tool metadata and the adapter cannot change it. The exact Bevy adapter embeds the same prediction provenance and unchanged engine-addressability evaluation, so it creates neither a second selector rule nor a second check lifecycle. Absent and repeated source names remain metadata. Scene/default-scene, skin, target path or UUID, named-map-winner, and extension-handling claims require new raw facts and a later profile revision and are outside this V1.

Issue #488 adds those rich facts as a separate immutable producer contract, urn:animsmith:schema:gltf-addressability:2; the historical V1 root and readers remain unchanged. V2 preserves the V1 animation inventory and adds bounded same-load raw glTF scene, node, skin, node-to-skin attachment, scene-path, default-scene, named-map, and unique animation-target evidence. Coverage is independent for every domain: complete empty coverage proves absence, while a retained prefix or unavailable domain never does. Rows retain source-array indices, authored names, parent/child order, scene roots, ordered skin joints, explicit skin.skeleton, and the dependency-closure identity. The explicit skeleton member remains source evidence only because Bevy ignores it; V2 makes no inferred-root claim and does not claim scene-instantiated SkinnedMesh attachment.

The optional exact-Bevy adapter is paired with the existing one engine-addressability evaluation and reuses the exact Animation{i} primitive, without creating a second check lifecycle. Its separately versioned rule bundle accompanies immutable Bevy profile revision 3 and pins tag v0.19.0, commit c6f634ca9f406d68ba5109d921247b654cb42c10, bevy_gltf 0.19.0, locked gltf 1.4.1, and the label, loader, node-path, target-id, feature, and root Cargo.lock sources. Scene{i} is emitted for every declared scene; Gltf.default_scene is only a route to an existing scene handle, with no DefaultScene label and no fabricated Scene0. Skin{i}/InverseBindMatrices is eager for every source skin, including unreferenced skins, using Bevy's identity fallback for an absent accessor. Skin{i} is created when any source node references the skin during the all-source-node construction pass. Source indices, not collected vector order, are identity. Named maps are separate, source-order last-write-wins projections; skin winners follow lazy first-reference order.

Target rows are per unique source animation target node and retain their animation/channel contributors. Authored names or GltfNode{source_index} fallbacks form the path, excluding the scene world-root. A path/UUID is published only with complete hierarchy, reachability, closure, feature, settings, and collision evidence. Unreachable or multiply reachable targets, duplicate paths, UUID collisions, disabled/missing bevy_animation, disabled load_animations, missing target pointer width, and incomplete evidence are typed required-unavailable states. Exact target IDs require explicitly pinned 32- or 64-bit pointer width because Bevy hashes segment lengths with the target pointer width's little-endian encoding; the host width is never inferred.

The projection carries explicit target_coverage separate from retained target rows. It is complete, including for an empty target domain, or is required-unavailable when raw node/scene/path or animation/channel evidence is incomplete, and with target_domain_truncated at the 4,096-target row ceiling. Other new-rich-projection bound failures use projection_bounds_exceeded. Each new scene/node/skin/attachment/path/target/ map domain is bounded at 4,096 rows, with aggregate structural references at 65,536 and dynamic projection text at 1 MiB; the sealed V1 animation inventory and CheckEvaluation scopes retain their own bounds. Names and path segments are capped at 1,024 UTF-8 bytes, paths at 4,096 bytes and 256 segments, and staged reports at 256 MiB. Overflow retains an independent canonical prefix; strict readback rejects N+1 collections and contradictory states. A bounded second parse of already captured primary bytes is permitted within the same loader invocation; reopening the primary or dependencies is not. V2 is prediction evidence only and never certifies runtime loading, scene spawning, target survival, graph wiring, or playback. Required- unavailable prediction retains exit 1; malformed tuple/config/input remains exit 2.

The second bounded 0.4.0 standalone producer is generate import-advice. animsmith-engine owns its independent V1 envelope, canonical identity, and strict reader. It embeds the same exact prediction-provenance record but is not a check evaluation: it projects fully materialized profile settings into document/per-clip importer properties beside same-load source identity, dependency closure, source-to-normalized clip linkage, explicit loop/movement intent, and normalized measurement availability. Unity 6000.3 Generic and Humanoid revision 1 can project Convert Units, Bake Axis Conversion, Generic Root Motion Source, and the three bake/extract root controls. Frozen Unreal 5.8 and Godot 4.7 revision 1 have no setting descriptors and therefore emit a typed refusal rather than a guessed preset. Authored frame coordinates, sampling policy, root-motion prediction, and unit-conversion inference remain out of scope until their own exact source/profile authorities exist.

Revision 2 of the producer is an independent immutable contract, urn:animsmith:schema:engine-import-advice:2. It uses output-v15's V4 prediction-provenance and prediction-basis types by reference and carries one optional native projection. available requires that projection and omits refusal_reason when the same-load dependency closure is complete; partial or unavailable closure requires refused, dependency_closure_incomplete, and no projection. Unknown tuples, missing settings, and unsupported formats are configuration errors, so neither those errors nor incomplete closure can be mistaken for an importer preset.

The only V2 advice profiles are exact: Godot 2 / 4.7 / resource-importer-scene accepts glTF JSON and GLB and projects animation/fps (1..120, verified default 30) plus animation/trimming (verified default false); Unreal 2 / 5.8 / fbx-importer accepts FBX and requires explicit sample_rate from default_30, source_determined, or custom_hz(1..48000). The projection retains explicit/default origin and no other settings. It is a parameter projection from same-load evidence, not engine execution, imported-asset readback, runtime/gameplay certification, or project-file mutation.

[checks.<id>] remains the only authority for severity and explicit enable/disable overrides. Existing checks such as loop-closure, in-place, foot-slide, and root-motion-speed keep their current behavior under every engine profile.

The shipped narrow slice is Bevy revision 3 / 0.19.0 / gltf-asset-loader engine-track-support. It binds a bounded raw animation inventory from the same load, with animation coverage and independent per-animation channel coverage. A complete inventory can produce only negative gate outcomes for rows dropped by the compiled bevy_animation feature or by load_animations; a dropped row is not a content finding. The compiled feature gate has precedence over the load setting. A complete-empty inventory is not applicable. Partial or unavailable coverage emits exactly one subjectless, unsuppressible required_prediction_unavailable inventory facet and no retained-prefix prediction, including the N+1 boundary. If both gates allow loading, the result is a stable required-unavailable runtime-survival state. Extensions, other constructs, and positive runtime survival are not modeled by this slice. V5 provenance and output-v19 are current for this Bevy slice; V4/revision-2 and output-v15 remain preserved and readable. The Unity Generic root-motion slice uses V6 provenance in output-v19, while output v9 and all other historical readers remain immutable.

Configuration precedence is consequently:

  1. loader and measurement evidence establish what the input contains;
  2. the resolved engine profile supplies immutable verified consumer facts;
  3. resolved document and per-clip importer settings state the configured consumer behavior, while [clips] fields state the intended use;
  4. [checks] supplies activation, severity, and tolerances; and
  5. the runner evaluates each applicable facet or records its typed required-unavailable prediction state.

No later layer may fabricate evidence missing from an earlier one.

E.4 Reproducible output and fail-closed coverage

measure remains engine-profile-independent: selecting an engine profile or its importer settings does not change measurements. Existing measurement inputs from [clips] and engine-neutral check policy retain their current semantics. lint and generated-advice outputs that use a profile record, at minimum:

  • requested family, profile revision, engine version, and importer;
  • resolved immutable profile URN and engine-facts digest;
  • input format, raw-source coverage, and fully materialized document and per-clip importer settings;
  • measurement and outer-output schema identities consumed;
  • each check id plus facet scope, exact fact/policy/measurement basis, and primary-source reference set, including clean evaluations; and
  • scoped required-unavailable reasons for prediction evidence that is missing, distinct from ordinary engine-neutral coverage gaps.

Output v17 publishes this record; output v9 remains immutable historical evidence. Under Appendix C, animsmith-core owns the outer lint envelope, its URN, and the registry-independent prediction/provenance wire types. animsmith-engine constructs those wire records through a one-way adapter without making core depend on the engine crate. The exact JSON shape is fixed by the v10 contract and permits a consumer to distinguish a measured fact, a profile prediction, generated advice, and an engine readback. Text and HTML views render that model rather than maintaining separate conclusions.

Prediction, advice, and readback contracts reuse one versioned provenance header for input dependency-closure identity, profile tuple and facts digest, settings digest, schema identities, and source references. The primary-file InputIdentity remains present, but it is not sufficient closure identity for a text glTF or any source with external dependencies. The header also carries a deterministic source-reference-to-resource map and the exact byte count and digest of every distinct external resource declared or consumed by the loader or target importer, keyed by a source-relative declaration rather than a host absolute path. It records closure coverage as complete, partial, or unavailable. If an applicable rule, suggestion, or readback comparison depends on a resource whose identity cannot be established, that work is unavailable rather than silently bound to primary-file bytes alone.

The three contracts' top-level schema identities and typed payloads remain separate because their authorities differ: a shared header must not let advice or an observation deserialize as measured or predicted evidence. This centralizes common identity plumbing without coupling the contracts' independent compatibility rules.

Profile selection never turns unavailable evidence into not_applicable. not_applicable is reserved for a rule whose subject genuinely does not exist under the resolved profile and project intent. An engine prediction is required when the resolved profile and project intent make its rule applicable, the run's CheckSelection selects its check, and the check remains enabled after [checks] configuration. An unselected check remains visible as unselected and is not required; an explicitly disabled check remains visible as disabled. Once a check is selected and enabled, severity does not change whether evidence is required. A required facet with schema-valid unavailable measurement evidence or incomplete raw-source coverage emits typed required_prediction_unavailable and makes lint exit 1. Malformed or non-finite present measurement data remains a contract/operator error and exit 2; it is not prediction unavailability. The facet is not rewritten as a content finding, and existing engine-neutral coverage gaps remain nonblocking. Advice generation refuses the affected suggestion without changing lint status. Unknown profile, version, importer, or setting selections fail earlier as configuration errors; they are never downgraded to coverage. Output v10 records the requirement state so embedded consumers can apply the same policy without parsing CLI exit behavior.

E.5 Root motion, units, and scale

Profiles keep these independent:

  • the source/interchange linear unit captured from normative format semantics or an explicit declaration in the source itself;
  • the target engine's native distance unit;
  • the exact source-to-target distance mapping and whether the importer preserves physical dimensions;
  • source and target handedness, up/forward axes, and the importer's selected axis-conversion behavior;
  • the resulting component/root, mesh-node, selected runtime-facing joint, and attachment transform scales; and
  • the project's movement-ownership policy.

For example, a centimetre-native engine does not imply a component scale of 100, and a metre-native format does not prove that a source hierarchy has unit effective scale. Rules consume the coordinate-domain and selected-node measurements designed in issues #267 and #268. They may recommend an importer setting only when its documented behavior accounts for animation translations, bones, sockets, attachments, IK targets, and collision anchors. Version one has no TOML or profile override for source units. A loader records the format-defined or source-declared unit and its provenance in the shared raw projection; if that evidence is absent, an applicable unit prediction is unavailable under E.4 rather than inferred from bounds, character height, filenames, importer defaults, or a caller assertion. They never infer units from bounds, character height, filenames, inverse-bind magnitudes, or an observed persistent object scale. Animation scale channels are dimensionless and never become length-bearing merely because the target engine uses a different world unit.

The contract can therefore state, independently, that one source metre maps to 100 target centimetre units, physical dimensions are preserved, and the expected imported component/root, mesh-node, and selected-joint transform scales are dimensionless 1.0. A mismatch in any one of those claims cannot be hidden by satisfying another.

Issue #269's accepted Appendix D result is incorporated here: whole-document unit conversion and rest/bind hierarchy reparameterization are distinct operations with separate positive-uniform, closure, and proof contracts. Profile selection neither authorizes nor freezes an automatic normalization path. It may diagnose the need for, or advise a separately requested, Appendix D operation only when that operation's own source coverage and proof requirements are satisfied.

Root motion is handled the same way. The profile describes which source or projection a configured importer uses and what a bake/extract option means. The artifact supplies engine-neutral horizontal displacement, signed vertical displacement plus non-collapsing vertical excursion/extrema, and yaw evidence. [clips.<selector>] independently declares movement_owner_xz, movement_owner_y, and movement_owner_yaw as either "gameplay" (the entity/controller owns world motion and the importer bakes that component into pose) or "animation" (extracted root motion owns world motion). Issue #466 owns these engine-neutral clip-intent fields. The existing in_place field remains a compatibility alias only for horizontal ownership (true means gameplay, false means animation); declaring both spellings in one selector entry is a typed configuration error. Each valid entry is normalized before the existing field-by-field exact/glob overlay, so an alias in one layer and the canonical field in another remain valid. A missing axis declaration makes only the intent-dependent rule for that axis not applicable; it is never inferred from another axis. A filename such as _RM, small translation magnitude, or the profile alone cannot declare intent. Conversion between root motion and in-place remains ADR-gated and is not authorized by this profile design.

E.6 Advice, manifests, and readback

Generated import settings use a separate immutable advisory-sidecar contract. They bind the complete input dependency-closure identity, full profile identity and facts digest, selected importer settings, measurement/check identities, and every suggestion or refusal. They must say when upstream canonicalization is required instead of presenting an unsafe importer bake as equivalent. They never affect lint exit status or claim to be measured evidence or the engine's actual imported state.

The 0.4.0 generation boundary is one input document. The engine-neutral glTF inventory may publish source indices, names, targets, and the complete input dependency-closure identity. A versioned Bevy adapter may derive typed labels and supported-extension predictions from that inventory; those fields are not mislabeled as engine-version-agnostic. Cross-file clip identity, runtime sets, collection manifests, and collection-wide presets consume the file-scoped contract from issue #409 in the 0.5.0 milestone rather than inventing a second identity here.

Engine readback has another immutable contract that records the exact engine build, project/import-settings digest, observed outputs, and warnings. Smoke tests compare a versioned prediction with that captured readback; successful parsing alone is not proof that an expected clip, target, or setting survived import. Engine SDKs, editor installations, licenses, and runtime harnesses stay outside animsmith-core. CI fixtures must be synthetic, self-authored, or redistribution-safe. Commercial packs may supply local, one-time validation evidence, but are never repository fixtures, workflow downloads, caches, or uploaded artifacts.

E.7 Primary-source and version discipline

Engine behavior is mutable external state. Every built-in rule cites a primary engine or specification source, records the verified target version and date, and is rechecked when that target or rule changes. Marketplace observations and local import probes are useful counterexamples, not authority for a general engine claim.

The design was reconciled on 2026-08-20 against these current primary sources:

Unversioned latest documentation is not retained as profile evidence.

E.8 Implementation sequence and non-goals

The dependency order is:

  1. this decision record;
  2. coordinated engine-neutral measurement work for per-property coverage and horizontal/vertical root trajectory plus yaw (#402 and #408);
  3. independent per-axis clip movement-ownership intent (#466), required before root-motion prediction but not by the measurement/profile substrate;
  4. the dedicated raw importer-sensitive source-facts projection (#463);
  5. strict engine-profile registry, config, and resolution types without prediction behavior (#464);
  6. bounded, rooted dependency-closure capture from the raw resource domain (#475);
  7. reproducible output-v17 provenance for the resolved target, facts digest, required-prediction state, and per-check basis (#465 and #482);
  8. per-concern prediction rules, split from umbrella issue #154 into bounded engine/rule slices where their input facts differ, including the Unity Generic V6 root-motion path;
  9. single-document preset advice and separate glTF-inventory/Bevy-adapter generation (#155 and #156);
  10. prediction-versus-readback feasibility and harness decisions (#151); and
  11. per-engine guides generated from the accepted rules and evidence (#157).

The two measurement tickets share schema, diff, rendering, and contract surfaces, so they require one version plan and sequential implementation rather than sibling branches. They do not depend on profile selection. Bevy graph- template demand (#163) remains gated by the manifest and a demonstrated consumer; the profile design does not turn it into an implementation commitment.

This decision does not add engine dependencies, custom TOML profile definitions, automatic unit or root-motion conversion, retargeting, compression simulation, artistic thresholds, cross-file collection policy, or claims of visual, deformation, artistic, gameplay, or runtime correctness. Those require their own evidence and, where they rewrite motion, a separate accepted design and proof boundary.

Appendix F — decision record: file-scoped clip identity and collections

Issue #409 decides the boundary used when logical clips and runtime relationships span physical source files. The decision is intentionally a protocol decision followed by bounded implementation slices. It does not make the existing multi-file spelling of lint collection-aware.

F.1 Separate collection authority

AnimSmith provides two independently versioned contracts:

  • urn:animsmith:schema:collection-manifest:1, a strict TOML declaration; and
  • urn:animsmith:schema:collection-output:5, deterministic JSON evidence.

The historical urn:animsmith:schema:collection-output:1 and urn:animsmith:schema:collection-output:2 contracts remain immutable. V2 derives its exact shape, budgets, and reader rules from V1, changing only the nested ordinary output envelope identity from output v10 to output v11. The historical urn:animsmith:schema:collection-output:3 derives from V2 and adds one bounded dependency_closure state to every source. A complete state retains the loader's exact DependencyClosureIdentityV1; partial and unavailable states retain a nonempty, sorted, unique sequence from the seven closed DependencyClosureCoverageReasonV1 values. The current collection-output:11 embeds the current output-v19 lint envelope with measurements-v18 while preserving collection-output-v8 and its nested output-v16, and collection-output-v4 with its nested output-v12/measurements-v15, as immutable historical evidence. Only complete closure coverage can establish a source, logical clip, or runtime-set member. This keeps primary input identity, optional manifest digest pin, and complete loader-input identity separate.

They are consumed and produced by an explicit command:

animsmith collection lint COLLECTION.toml --format json

The command name, input identity, and output identity are distinct from ordinary animsmith lint FILE.... Existing lint and measure invocations remain document-local and continue to emit output-v13 with measurements-v16. A file extension, the presence of multiple inputs, or repeated embedded take names never switches commands implicitly.

The collection manifest is the sole AnimSmith authority for:

  • durable logical clip identifiers;
  • physical source and take bindings;
  • optional source digest pins; and
  • cross-file runtime-set identity and membership.

It is not a second authority for rig roles, per-clip expectations, check severity, or check tolerances. Those remain in an explicitly selected existing animsmith.toml config basis and retain their ordinary per-document selector semantics. The collection manifest may select a complete config basis for a source; it cannot override individual config fields.

An assembly recipe is not a collection manifest. Assembly is a mutating producer that chooses a base, projects source domains, and publishes one artifact. Collection lint preserves each physical source and evaluates relationships without renaming, merging, or rewriting it. Likewise, the animation-pack evaluation manifest remains a skill-level evidence and taxonomy format. It may supply retained validation evidence, but it is not accepted as the CLI protocol.

The repository-owned structured evaluation model is a separate skill-level authority. animation-pack-evaluation:1 remains frozen to independently validated collection-output:2. Current evaluations use the additive animation-pack-evaluation:2 contract and bind the exact raw collection-output:11 digest and byte count plus its manifest identity. V2 retains every scrubbed source state, including complete dependency-closure identity or the full typed partial/unavailable reason set, and proves exact clip source/take and ordered runtime-set relationships against that V11 input. The V2 validation boundary requires the explicitly selected checkout-matched AnimSmith binary and invokes a hidden validation-only collection subcommand that calls the one authoritative V11 strict reader before any projection. Schema validity plus a matching raw digest is not sufficient, and there is no second Python semantic mirror. Clip duration and speed preserve exact measured, unavailable, or not-applicable availability rather than collapsing the latter two categories. Unavailable rows are mandatory typed soft failures, not grounds to shrink the model to surviving records. A separate projection sidecar was rejected because it would duplicate clip/set authority and create a third versioned contract; V2 is the single report authority, while the exact hashed V11 document remains the mechanical evidence binding. Neither model is accepted as a CLI protocol.

F.2 Logical and physical clip identity

Every collection clip has one user-declared logical id and one physical binding. Neither is sufficient alone.

The logical id is the durable public reference used by runtime sets and future sidecar schemas. V1 ids are portable, lowercase ASCII, slash-separated tokens:

com.example.pack/locomotion/walk-forward-in-place

Each token starts with [a-z0-9] and then contains only [a-z0-9._-]; empty, . and .. tokens are invalid. An id has at least two tokens and at most 255 bytes. The namespace is a collision-avoidance convention, not an ownership or compatibility claim. It is opaque to AnimSmith: filenames, directories, take names, vendor metadata, and clip behavior never generate or rewrite it.

The physical binding contains:

  • one manifest-local source key;
  • a zero-based source-local take index; and
  • the exact expected embedded take name at that index.

Index and name are both required. Name alone is ambiguous when many files or multiple takes use Take 001; index alone would silently accept reordered or renamed source content. The source record supplies the safe root-relative locator, observed primary InputIdentity (SHA-256 and bytes), optional expected SHA-256, complete or typed-incomplete dependency-closure state, and config basis. A source rename does not change the logical id, but the manifest must be edited explicitly and output records the changed physical locator. A digest pin is a binding assertion, not the logical identity.

One physical (source, take_index) may have only one logical id. Two ids bound to the same physical take are an ambiguous duplicate and invalidate the manifest. One source may legitimately bind several distinct take indices. Distinct source locators with equal content digests remain distinct physical sources; content equality does not create an alias or compatibility claim.

F.3 Manifest V1 shape and composition

The normative implementation issue freezes the exact JSON Schema and Rust wire types, but V1 has this authority shape:

schema = "urn:animsmith:schema:collection-manifest:1"
schema_version = 1
collection_id = "com.example.pack"
input_root = "assets"

[[sources]]
key = "walk-forward"
path = "locomotion/walk_forward.fbx"
config = "configs/locomotion.toml"
expected_sha256 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"

[[clips]]
id = "com.example.pack/locomotion/walk-forward-in-place"
source = "walk-forward"
take_index = 0
take_name = "Take 001"

[[runtime_sets]]
id = "com.example.pack/sets/walk-ring"
kind = "directional-blend"
members = [
  "com.example.pack/locomotion/walk-forward-in-place",
  "com.example.pack/locomotion/walk-left-in-place",
]

The schema/version pair, collection id, nonempty source/clip arrays, and every field shown for clips are required. input_root, source config, and expected_sha256 are optional. An absent config means Config::default(); there is no ambient config discovery. A selected config path applies to the whole loaded source document. The same source cannot select different configs for different takes.

The collection id and source keys use one token from the lowercase ASCII logical-id grammar (no /) and are each limited to 255 bytes. Source keys are manifest-local plumbing rather than public clip identity. Every clip and runtime-set id must start with the exact collection id followed by /; this makes the declared collection namespace an enforced prefix without treating it as an ownership claim. Unknown fields, duplicate ids/keys, duplicate source/take bindings, malformed digests, dangling source references, dangling or repeated set members, and sets with fewer than two members are operator errors.

Runtime-set ids use the logical-id grammar. V1 set kinds are membership vocabulary only:

  • gait-group;
  • sync-group;
  • directional-blend;
  • speed-blend;
  • transition-chain;
  • mask-composition;
  • retarget-group;
  • paired-interaction; and
  • motion-database.

The closed list prevents typo-created semantics. A kind does not itself claim that its members blend, retarget, synchronize, or play correctly. V1 preserves declared member order because some relationship consumers assign coordinates or roles by declaration order. Output sorts source, clip, and set records by their stable ids, while each set retains its declared member order.

V1 intentionally carries no generic policy map and no untyped TOML payload. Check-specific coordinates, tolerances, or controller policies require a typed, versioned contract owned by that check. They must not be smuggled into collection V1 as open data merely to avoid a schema revision.

The parser and wire reader are bounded before allocation. The implementation slice must freeze limits for manifest bytes, source rows, clip rows, runtime sets, aggregate memberships, identifier bytes, and output bytes, with exact N/N+1 tests and checked arithmetic. Safe path spelling reuses the immutable dependency-closure V1 key limits: 4,096 UTF-8 bytes and 128 components.

F.4 Rooted access and immutable sources

The manifest directory is the control root. input_root, when present, is a relative directory below it; source paths resolve below that input root. When it is absent, source paths resolve below the manifest directory itself, never the process working directory or another ambient root. Config paths always resolve below the manifest directory. All declared paths use / as the separator and are retained as UTF-8 evidence labels, never as host absolute paths.

The CLI/frontend owns filesystem access. It reuses DependencyResourceKeyV1 with ParserRelativePath for lexical validation and uses a rooted resolver equivalent to the assembly/material boundaries. It rejects, before source execution:

  • empty, ., .., absolute, drive-qualified, UNC, remote, or backslash paths;
  • NUL, control characters, oversized keys, and excessive components;
  • a symbolic-link component or final symbolic link;
  • a non-directory input root, non-regular config target, or existing non-regular source target; and
  • a canonical target outside its declared root.

Case and Unicode are preserved exactly; AnimSmith does not case-fold or Unicode normalize a declaration into another identity. Lexical duplicates and aliases the host resolver exposes as one canonical target are rejected. V1 does not claim portable hard-link identity beyond what the host safely exposes.

The source is opened read-only, measured, and never renamed or rewritten. Observed identity is derived from the bytes actually analyzed. An expected digest mismatch is evidence about a readable binding, not permission to use the bytes as if they matched. Root containment and symlink refusal prevent ordinary path escapes; they do not claim to defeat a privileged actor concurrently mutating the filesystem between checks and reads. That portable TOCTOU limit is retained in the public security notes.

F.5 Existing config and document-local checks

Collection execution loads each source with exactly one selected existing config basis. [clips."<selector>"], [gait_groups], and [sync_groups] continue to resolve embedded clip names inside that one document. Reaching a config through a collection does not make its selectors match logical ids and does not reinterpret a previously valid single-document project.

Document-local gait/sync checks may run as part of the nested source analysis. Collection runtime sets are separate records keyed by logical ids. A shared name does not merge the two domains, and membership is never inferred from a document-local group, filename, directory, or evaluation taxonomy.

Core remains format- and filesystem-neutral. It may own validated bounded logical/source/take/set values and deterministic evidence types. TOML parsing, rooted access, loader dispatch, and command exit routing remain in the CLI frontend. Format loaders continue to own their raw-source and dependency closure projections.

F.6 Collection output and completeness

collection-output:1 is a historic strict envelope, not ordinary output v12. It contains:

  • the manifest schema identity, observed manifest SHA-256/bytes, collection id, tool version, and frozen collection budget identity;
  • one source record with its safe declared locator, observed or unavailable source identity, expected-digest result, config state/path/digest, loader state, and nested ordinary per-document result when available;
  • one clip record with logical id, source reference, take-index/name witness, binding state, and exact nested measurement/check references;
  • one runtime-set record with id, kind, every declared member, each resolution state, an explicit evaluation lifecycle, coverage gaps, findings, and any later check-specific measured payload; and
  • deterministic totals and terminal work counters needed to prove bounds.

Nested ordinary results retain their published schema identities and strict readers. They are not copied into a nearly equivalent collection-only shape. Host absolute paths and raw host I/O messages are never emitted. A source row uses its safe declared locator; stable typed states carry failures.

Collection-output V1 additionally carries one existing ClipMeasurements value per established logical clip, selected from a duplicate-safe normalized-index projection only after the raw source take index and exact authored name map to that normalized index. This additive in-memory projection does not change measurements-v15: ordinary output remains name-keyed. When two authored names remain equal after loader normalization, their indexed measurements remain distinct while a name-addressed check reference is typed unavailable; no first/last match is guessed. Runtime-set V1 completeness depends on the binding and indexed measurement, not that optional check reference, and its decision remains not_evaluated until a later typed set check owns a conclusion. The normalized name is bounded at 4,101 bytes: the 4,096-byte authored-name maximum plus # and the largest duplicate ordinal possible under the 4,096-clip manifest cap. Available nested measurement keys retain output-v10's 4,096-byte bound. When that derived key exceeds output-v10's immutable 4,096-byte text bound, V1 retains physical binding and indexed measurements but marks the nested document and name-addressed check reference nested_output_unavailable, then exits 1; it never embeds a schema-invalid ordinary envelope.

The immutable output budget permits 1 GiB per primary source, 16 GiB of aggregate primary-source reads, and 256 MiB of serialized/readback JSON. It reuses the manifest V1 source, clip, runtime-set, aggregate-membership, and aggregate-work limits. Primary reads and strict output readback stop at N+1; checked work counters retain the terminal observation needed to distinguish an exact limit from exhaustion. Producer serialization also writes through a 256 MiB N+1 sink before any full envelope can be allocated, and its self-reported byte count converges over the finite decimal widths permitted by that same cap. After an aggregate primary-read N+1 witness, later declared sources are retained as aggregate_exhausted without being opened, so the terminal counter cannot advance beyond that single witness.

The manifest digest binds the exact input bytes. Reordering otherwise equivalent TOML therefore changes that input identity, while the decoded source, clip, and set record arrays remain canonically ordered and byte-identical. The contract does not erase source-manifest changes merely to make two different inputs share a full output digest.

A set may report a complete conclusion only when every declared member needed by that conclusion resolved and supplied the required evidence. Missing, rejected, mismatched, quarantined, or unmeasurable members remain visible. The evaluator must not shrink a set and report a clean result over the survivors. Member ordering in output is deterministic and every group-level measurement names its exact member basis.

Logical ids, equal source digests, common skeleton names, or membership in one set prove none of retargeting, blend, artistic, deformation, engine, gameplay, or cross-pack compatibility. Those claims require their own declared policy and evidence.

F.7 Failure and exit policy

The collection command introduces one explicit aggregate-data exception to the ordinary lint operator-error rule. The exception is scoped to a manifest that has already passed all control-plane validation:

  • exit 2 with no collection envelope for an unreadable or malformed manifest, unsupported schema identity, budget violation, duplicate/unsafe declaration, existing non-regular source target, missing/unreadable/malformed selected config, serialization failure, or other control/tool error;
  • exit 1 with a schema-complete collection envelope when a lexically safe declared source is missing or unreadable, when readable bytes are unsupported or malformed, when a digest/take binding mismatches, or when another required collection member cannot be established; and
  • otherwise apply the existing lint severity and required-unavailable rules to available nested/check evidence, with exit 0 only when none requires 1.

Missing/unreadable bytes and readable-but-rejected bytes have distinct typed states even though both make a required collection incomplete. Unsafe paths and configs are never downgraded to data availability. This preserves all declared members for batch automation without silently changing ordinary lint FILE..., which retains exit 2 and no output for an unreadable primary file.

An unavailable measurement inside a successfully loaded member follows the owning check's existing coverage/required policy; collection membership alone does not promote every nonblocking coverage gap to exit 1. A source that cannot be loaded at all is different: the declared physical clip identity was not established, so the collection result is incomplete and exits 1.

F.8 Retained evidence exercise

The retained, externally generated animation-pack evidence behind #409 contains the required marketplace shape: many physical files repeat one embedded take name, while meaningful logical motions and runtime sets live outside those takes. The public eight-pack rollup reports 895 individual files, 582 logical motions, and 90 runtime-set records without publishing licensed source bytes.

The mapping exercise is lossless for the foundation facts:

  • retained per-file locator and digest facts map to source rows;
  • retained logical-motion identities map to namespaced clip ids;
  • selected source-local takes map to the index-plus-name witness;
  • retained runtime-set membership maps to logical-id member lists; and
  • evaluator roles, confidence, capability profiles, workflow stages, report prose, and engine observations deliberately remain outside this contract.

This shows why the evaluation manifest must not be reused wholesale. It proves the collection identity and membership need while keeping evaluation taxonomy and product conclusions at their existing skill/report authority.

The retained license-safe spike fixture is crates/animsmith/testdata/collection-spike. Its self-authored, motion-free glTF files cover two distinct sources with the same bytes and embedded Take 001, one two-take source, cross-file gait and sync sets, an explicit config basis, and duplicate-member, missing-member, and escaping-source failures. The preservation table pins each locator, byte count, digest, logical id, and index-plus-name witness without treating that retained fixture as output-schema authority.

Repository tests use only synthetic/self-authored or explicitly redistribution-safe multi-file assets. Licensed pack bytes and motion-bearing derivatives remain outside source, CI, caches, logs, and public artifacts.

F.9 Implementation sequence and non-goals

The 0.5.0 order is:

  1. this accepted decision record and bounded issue decomposition (#409);
  2. strict collection-manifest V1 types, budgets, logical/physical identity validation, and rooted resolver with license-safe fixtures (#545);
  3. explicit collection lint, deterministic collection-output V1, nested per-document execution, aggregate failure routing, and strict reader tests (#546);
  4. contact-sidecar and transition-family schema spikes (#147 and #148) using the logical clip-reference vocabulary without generation code;
  5. truthful gait-phase statistic publication (#504), after separately choosing the existing maximum circular deviation from mean or a deliberate changed threshold statistic; and
  6. root-speed/stride set evidence (#411), split so raw evidence and any typed controller policy do not expand collection V1 into an open policy map.

The parser/resolver and execution/output work are separate PRs because rooted I/O review and public output/exit review have different failure surfaces. Both must land in 0.5.0; closing the ADR alone does not deliver the milestone's collection foundation.

This decision does not add collection report HTML, generated transitions, engine graph output, archive extraction, filename-based intent inference, source renaming/merging, motion rewriting, cross-pack compatibility claims, or a generic untyped runtime policy language. The strict contact-fragment sidecar producer specified in F.10 is the narrow exception; host-side merge and all runtime interpretation remain later consumers or separately accepted contracts.

F.10 Contact-fragment V1 (#147)

Issue #147 freezes the interchange contract for contact facts. In 0.6.0, issue #152 delivers its narrow strict producer: animsmith generate contact-fragment and animsmith collection generate-contact-fragment sample stance support and atomically publish one source-bound fragment. The collection command reloads its selected manifest source rather than consuming collection-output evidence. Neither issue adds host-side sidecar merge or a runtime event system. Its independently versioned identity is:

urn:animsmith:schema:contact-fragment:1

The fragment is an importable envelope, not a second independently edited authority beside a host's measured sidecar. A V1 envelope contains schema, schema_version, producer (tool and tool-version), artifact (the exact primary source bytes' existing InputIdentity wire {sha256, bytes}), dependency_closure_identity (the complete existing DependencyClosureIdentityV1, serialized through that same {sha256, bytes} wire), one clip reference, a measured positive duration_s, and an ordered events array. The sha256 field makes both the algorithm and digest explicit; this contract does not introduce an {algorithm, digest, bytes} shape. Both identities are mandatory. The closure identity binds the primary source and every dependency in the loader's complete versioned dependency domain; partial or unavailable closure coverage refuses fragment generation. This is the identity only, not the full dependency_closure record used by output v10. The captured closure's primary_input must equal artifact; the producer and any consumer checking staleness validate that relationship against the complete captured closure. Unknown fields are rejected by the strict core reader implemented in 0.6. A mismatch in either identity makes the fragment stale rather than authorizing a consumer to use it.

AnimSmith implements the format-neutral strict reader and bounded canonicalization/identity seam in animsmith-core, plus the strict producer above. The producer loads one exact direct take or reloads one exact manifest-selected take, requires complete finite bilateral stance evidence, and publishes the canonical fragment as a single durable sidecar. It does not implement contact transforms, host-side merge, runtime scheduling, engine mapping, physical-contact inference, or gameplay semantics; those consumer boundaries remain separately deferred.

V1 deliberately requires the complete modeled closure, including declared dependencies such as textures that may not affect contact calculations. It can therefore refuse generation when an unrelated dependency is unavailable. This conservative policy avoids a second format-specific dependency-relevance authority; narrowing it requires a separately versioned contract.

The clip reference is a tagged scope rather than a filename or animation-array index:

  • collection carries the #409 logical clip id together with its physical source, take-index, and exact take-name witness. The witness lets a host detect a manifest edit or take reorder even when a logical id was retained.
  • document carries the exact embedded clip/take name and the primary-plus- dependency-closure binding that scopes that name. If the document exposes duplicate names or cannot establish an unambiguous source-local take identity, the reference is unavailable and the fragment is refused; no first/last animation match is guessed.

An event has a stable event_id, an engine-neutral role and phase, and either a point time or a window {start, end}. Times are normalized to the closed interval [0, 1]; start <= end is required for a window. A point or window is never identified by its array position. Optional confidence and explicitly versioned extension data are non-normative additions; confidence, when present, is finite and lies in [0, 1]. Unity, Unreal, Bevy, and gameplay-specific event types do not belong in V1. An event_id is unique within the clip and remains stable when the same fact is regenerated. It is opaque to consumers, is not an engine handle, and is not derived from array position.

V1's closed role vocabulary is left_foot, right_foot, left_hand, right_hand, left_toe, right_toe, left_knee, right_knee, left_elbow, right_elbow, root, prop, and body. Its closed phase vocabulary is begin, end, and marker. A producer that needs another semantic role or phase must propose a versioned contract extension; it must not smuggle an engine enum or arbitrary string into V1.

Canonical bytes use the complete RFC 8785 JSON Canonicalization Scheme (JCS), including its object-member sorting, string escaping, and number serialization rules. Every value, including an opaque extension payload, must be JCS-canonicalizable or the fragment is refused; an extension does not supply a private key-ordering rule. The reader accepts decoded IEEE-754 numbers only within the safe magnitude [-9007199254740991, 9007199254740991], including identity byte counts and extension-payload numbers. Lexical decimal aliases are therefore the same decoded JCS value, not distinct V1 facts. Extension payloads are reparsed from their bounded JCS bytes before storage, so canonical serialization followed by strict reread preserves the validated fragment value. Before JCS serialization, a mixed point/window event sort key is exactly (start, kind_rank, end_key, role, phase, event_id): a point sets start = time, kind_rank = 0, and the end sentinel end_key = null, which sorts before every numeric window end; a window uses its declared start, has kind_rank = 1, and sets end_key = end. Thus a point and a window at the same start have a deterministic order, including when their numeric endpoints are otherwise equal. Every string comparison in that tuple, including the opaque event_id, is lexicographic by unsigned UTF-16 code units exactly as RFC 8785 orders object property names; no Unicode normalization is applied. Extension arrays retain their declared order. Seconds and frame numbers, if present for display, are derived values and never identity or comparison coordinates. The strict core reader rejects unknown fields in the core envelope and events, duplicate event ids, non-finite values, out-of-range times, and ambiguous references before accepting the fragment. These rules make a regenerated fragment reviewable without making event detection part of this contract.

V1 is bounded before retention or canonicalization. A contact-fragment source is at most 8,388,608 bytes of UTF-8 JSON with at most 32 nested arrays/objects, 4,096 events, and 256 extensions. Every authored string is at most 4,096 UTF-8 bytes; identifiers (including event_id, producer fields, schema ids, source keys, and logical ids) are additionally at most 255 bytes, except the exact 64-lowercase-hex SHA-256 spelling. One extension payload is at most 262,144 JCS bytes and 16 levels deep. The complete canonical fragment is also at most 8,388,608 JCS bytes. A contact-transform-result source and its complete canonical result are each at most 16,777,216 bytes; it contains at most 4,096 control points and 4,096 event outcomes and embeds a contact fragment obeying the preceding limits. Container depth is the number of object/array containers on the path from the root to a value, inclusive: the top-level envelope object has depth 1. An extension payload object participates in that full-envelope count and is also depth 1 for its separate 16-level payload limit. A strict reader accepts each exact maximum and rejects the first byte, element, string byte, or nesting level above it while decoding, before retaining that excess value or allocating an unbounded canonical buffer. Canonical byte limits are enforced with a bounded JCS sink. The published JSON Schema's maxLength limits are Unicode-code-point limits; the reader's UTF-8 byte limits are deliberately stricter for non-ASCII text.

Extensions, when present, are an array of strict envelopes. Each envelope has exactly schema, schema_version, and payload; schema is the extension's versioned identity, schema_version is a positive integer, and payload is a JSON object validated by that extension's schema. The core reader rejects unknown envelope fields and preserves a well-formed unsupported extension as opaque data; a consumer that requires its semantics may reject the fragment with a typed unsupported-extension result. Core fields remain closed and an extension cannot redefine them or add an engine event type.

A minimal collection-scoped envelope is illustrative of the normative shape (the two input identities are not second clip digests):

{
  "schema": "urn:animsmith:schema:contact-fragment:1",
  "schema_version": 1,
  "producer": {"tool": "animsmith", "version": "0.6.0"},
  "artifact": {"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "bytes": 123456},
  "dependency_closure_identity": {"sha256": "1111111111111111111111111111111111111111111111111111111111111111", "bytes": 456},
  "clip": {"scope": "collection", "logical_id": "com.example.pack/locomotion/walk-forward-in-place", "source": "walk-forward", "take_index": 0, "take_name": "Take 001"},
  "duration_s": 1.2,
  "events": [
    {"event_id": "left-foot/0", "role": "left_foot", "phase": "marker", "time": 0.23, "confidence": 0.92},
    {"event_id": "right-foot/0", "role": "right_foot", "phase": "begin", "window": {"start": 0.71, "end": 0.79}}
  ],
  "extensions": [{"schema": "urn:example:contact-quality:1", "schema_version": 1, "payload": {"quality": "high"}}]
}

The standalone document clip variant replaces the collection fields with clip_name and retains both envelope input identities. A host may deterministically merge this fragment into its existing sidecar, but the host remains the sole authority for that file's unrelated fields.

Trim, slice, resample, and time-warp are contract-preserving only when the contact fragment is transformed in the same operation. The minimal transform result has identity urn:animsmith:schema:contact-transform-result:1 and fields schema, schema_version, operation, input, outcome, event_outcomes, and, only when outcome = "transformed", output; a refusal additionally has a refusal object containing exactly a stable code and message for display. The strict tagged operation object has exactly these structurally representable V1 shapes:

  • {"kind":"trim","version":1,"interval":{"start":a,"end":b}};
  • {"kind":"slice","version":1,"interval":{"start":a,"end":b}};
  • {"kind":"resample","version":1,"mapping":"identity"}; and
  • {"kind":"time_warp","version":1,"output_duration_s":d,"control_points":[{"input_time":0,"output_time":0}, ...]}.

The control-point array is ordered, finite, starts at (0, 0), ends at (1, 1), and has strictly increasing input and output coordinates. The mapping is piecewise linear: an exact input knot maps to its declared output knot; for adjacent knots (x0, y0) and (x1, y1), an input t strictly between them maps to y0 + ((t - x0) / (x1 - x0)) * (y1 - y0). Point times and both window endpoints use that same mapping. Structural decoding retains a known V1 operation even when its finite numeric domain or ordering is invalid, so a refusal can echo the request. An unknown kind, version, field, mapping token, missing field, or malformed field type is a strict request/reader error and produces no V1 transform result or event outcomes. input has exactly {artifact:{sha256,bytes},dependency_closure_identity:{sha256,bytes},fragment:{sha256,bytes}} and refers to the separately supplied input fragment; the input fragment is not duplicated inline. A successful output has exactly {artifact:{sha256,bytes},dependency_closure_identity:{sha256,bytes},fragment:{sha256,bytes},contact_fragment}; its inline contact_fragment is the complete transformed urn:animsmith:schema:contact-fragment:1 value. The output fragment identity must equal the canonical serialized inline fragment's {sha256, bytes}, and the output artifact identity must equal the inline fragment's artifact binding. The output dependency_closure_identity must equal the inline fragment's closure binding and must be freshly captured for the output artifact rather than copied from the input. Its captured closure's primary_input must equal the output artifact. The output is absent, never a null placeholder, on refusal.

V1 operation mappings are exact: trim and slice carry a finite retained interval [a, b] with 0 <= a < b <= 1 and map an in-range t to (t - a) / (b - a); resample carries the identity mapping on normalized time; and time_warp carries the piecewise-linear control points above. A point outside a trim/slice interval has outcome outside and is omitted only from a successful output fragment. For a point, t < a || t > b is outside and both endpoints are retained. A window is wholly outside exactly when end < a || start > b, and is fully contained exactly when a <= start && end <= b; equality at either retained endpoint is contained. Every other window crosses a boundary and has outcome refused with code partial_window, refusing the entire operation—no partial window is silently clamped.

Preserving an opaque extension through an artifact transform cannot prove that extension-owned times or event references remain valid. A transformer may succeed only when it recognizes every extension schema/version and implements that extension's operation-specific transform contract. Otherwise it refuses the whole operation before event inventory with top-level code unsupported_extension, an empty event_outcomes, and no output fragment. The core transform context therefore carries the handler-produced extension outputs, in exact input order and with the same schema/version at every position. It never treats a caller-declared support set or an automatic opaque payload copy as proof that an extension was transformed.

The inline output fragment's duration_s is exact: trim and slice use input_duration_s * (b - a), resample preserves input_duration_s, and time_warp requires a finite positive output_duration_s field in its operation object, which the output fragment must equal. These duration rules apply even when every event is outside the retained interval. If the rounded trim/slice duration is not finite and positive, the operation refuses before event inventory with invalid_value and an empty event_outcomes list.

Every V1 time and duration number is a finite IEEE 754 binary64 value. Arithmetic uses round-to-nearest, ties-to-even after each operation in the following order; fused multiply-add and extended-precision intermediates are not permitted. Write rn(x) for that rounding. Trim/slice first computes span = rn(b - a), then mapped = rn(rn(t - a) / span) and output_duration_s = rn(input_duration_s * span). Between time-warp knots it computes dx = rn(x1 - x0), alpha = rn(rn(t - x0) / dx), dy = rn(y1 - y0), and mapped = rn(y0 + rn(alpha * dy)); an exact knot bypasses interpolation and returns its declared output_time. Resample returns the decoded input value unchanged. Any non-finite intermediate is invalid_value. These binary64 results are the numbers supplied to JCS.

After input binding and fragment identity validation, event_outcomes contains exactly one object per input event, in the fragment's canonical input order. Each object has exactly event_id and outcome, plus value only when outcome = "transformed" (the exact mapped point or window) and code only when outcome = "refused". A pre-inventory binding, fragment-identity, operation-validation, or extension-support refusal uses an empty event_outcomes list. Global success means outcome = "transformed", an output is present, and every event outcome is transformed or outside; global refusal means outcome = "refused", a typed top-level refusal is present, and output is absent. A refusal discovered after inventory has at least one refused event outcome. partial_window means the exact crossing predicate above; invalid_mapping means a structurally known operation has an out-of-domain interval, invalid endpoint/order, or non-monotonic control points; invalid_binding means the supplied fragment, primary artifact, or dependency-closure identity does not match current input; and invalid_value means valid finite inputs produced a non-finite arithmetic intermediate or a rounded trim/slice duration outside the fragment's finite positive domain. Unsupported extension transformation uses unsupported_extension as specified above. A malformed fragment/result or duplicate, missing, or unknown event-outcome identity is a strict reader error, not another refusal result. The result records the operation, bindings, and outcomes. Core implements the exact mapping and independently rederives a read result against its separately supplied input fragment and external transform context. That context carries the current input and output artifacts, their complete captured closures, the expected output producer, and any operation-specific transformed extension outputs. Core validates that each closure's primary input is its corresponding artifact before deriving the identity recorded in a successful result; detector policy, asset mutation, and publication remain outside this contract.

AnimSmith owns the strict sampled stance-support contact facts and these identity/time/transform semantics. The host owns final file layout and merge, unrelated measurements and provenance, runtime scheduling, and engine-native mapping. The delivered contact-fragment producer does not perform contact transforms, infer gameplay meaning, validate foot placement, or claim engine, artistic, or gameplay correctness. Host-side merge and runtime behavior remain deferred.

F.10A Skeleton compatibility V1 (#158)

Issue #158 adds a strict structural seam for declared source/target skeleton comparison without implementing retargeting, mesh rewrite, engine-avatar generation, masking, contacts, or gameplay proof. Its immutable JSON result identity is:

urn:animsmith:schema:skeleton-compatibility:1

The public command is animsmith skeleton compare SOURCE TARGET --correspondence FILE --format json|text. FILE is a separate strict TOML authority with independent identity urn:animsmith:skeleton-correspondence:1. It binds the exact source and target primary input identities, explicit selected roots and finite selected node-name sets on both sides, either exact-name or one-to-one explicit correspondence, and every numeric tolerance. Unknown fields, duplicate names or targets, malformed or stale identities, ambiguous selectors, non-finite tolerances, oversize control input, or source/target primary inputs above the existing 64 MiB bound are operator errors that emit no result.

The result records exact source and target primary identities, complete dependency-closure identities when same-load capture is complete, the exact correspondence bytes, canonical selected-skeleton provenance for each side, deterministic row order, and separate non-scoring evidence facets for source skin membership, inverse-bind accessors, and deformation model coverage. Topology/rest rows are the only scoring surface in V1. compatible means every required row was available and within tolerance; partial retains mixed available and unavailable required evidence even when another retained field mismatches; otherwise any required mismatch is incompatible; and not_evaluated means no correspondence row was evaluable. Missing source/target names and parent mismatch remain explicit typed rows rather than being collapsed into an aggregate score.

The seam compares only the declared selector members. It does not infer a skeleton subtree from attachments, scene reachability, or skin membership. Parent correspondence and normalized child length walk through unselected intermediate nodes to the nearest selected ancestor, so they describe the declared projection and do not claim complete raw-hierarchy equality. selected_skeleton_identity is derived provenance, not a copied authored profile: AnimSmith serializes a canonical, name-ordered projection of each selected node's measured structural evidence and the declared selector, then records that exact identity. Local and rest-world transform deltas reuse the existing normalized source-skeleton measurements contract; world-space comparison stays unavailable when the loaded source evidence does not prove a finite right-handed unit-orthonormal rest-world basis.

Non-pass rows and unavailable evidence sides carry a typed remediation surface and likely remedy class. This routes work to correspondence, the selected skeleton authority, selected skin/bind evidence, or a measurement boundary without guessing whether a project integrator, artist, or vendor is to blame. The human text view renders those fields together with exact identities, matching authority, tolerances, row deltas, and facet details.

F.10B Foot-cycle parameterization planner V1 (#18)

Issue #18's first post-result implementation slice adds the separate strict urn:animsmith:schema:foot-cycle-parameterization:1 TOML declaration and pure format-neutral map planner. The declaration binds exact manifest bytes, one existing ordered gait-group, one explicit reference, one safe fragment path per member, the future generation directory, and finite positive segment-slope bounds whose inclusive interval contains identity slope 1.0. It also requires a closed [proof] table whose finite maximum gait-phase spread and contact boundary phase error each lie in [0, 0.5], and whose finite minimum left/right amplitude is non-negative. These values are bound by the exact parameterization bytes and retained in the plan, with no defaults, config lookup, inference, or cross-member merge. The planner accepts only canonical collection-scoped fragments with the exact known stance-detector V1 provenance and independently measured typed Root/Hips evidence bound to the same exact artifact, dependency closure, and collection source/take witness. Missing, ambiguous, malformed, or non-finite root evidence refuses. It retains signed endpoint X/Z displacement and signed accumulated unwrapped yaw, while the unchanged admission applies binary64 horizontal hypot(X, Z) at 0.01 m and absolute yaw at 1 degree; this pure slice does not derive those source measurements. It partitions positive windows and markers by side, requires one marker per physical window and strictly disjoint same-side runs, and preserves those physical rows. A distinct same-side first run beginning at 0 and last run ending at 1 coalesce into one logical cyclic stance whose artificial seam edges do not enter correspondence; a full [0,1] stance refuses. Opposite-side overlap and touch are valid. Logical left/right counts must be equal and nonzero, with alternating onsets around the cycle. The planner builds the true logical boundaries in deterministic cyclic chronological order, rotates correspondence (not phases) to a true left onset, and requires one identical boundary signature across members. It pairs boundaries positionally and maps them to the reference's authored phases. Simultaneous boundary groups must agree on both source and reference instants; identical map pairs collapse before (0,0) and (1,1) are included and the retained 4,096-knot cap is checked. It returns bounded duration-preserving time_warp operations with exact contact input bindings. All members must use one exact detector contact height because that policy determines the compared boundaries. The whole ring admits at most 16,384 contact events and 32 MiB of canonical fragment bytes before topology retention and repeated canonicalization, and marker/window validation is ordered rather than cross-product work. Only the correspondence topology is circularly normalized: phase zero is not rotated, so incompatible wrap/non-wrap correspondence or any other mapping that cannot remain strictly monotone and endpoint preserving refuses. This slice does not load or mutate animation assets, transform fragments, derive its required typed in-place Root/Hips evidence, serialize candidates, prove output, or publish the collection generation.

F.10C Foot-cycle clip candidate V1 (#18)

The next independent slice is the pure format-neutral application of one planned member map to an already selected Clip. time_warp_clip_v1 validates track-local clip/track shape again, requires the plan's exact duration-preserving V1 operation, and returns a cloned candidate without mutating its input. This Clip-only seam has no skeleton input, so its host must already have bound every track bone index to the selected, validated skeleton. Normalized control points map source time to output time: LINEAR tracks retain all mapped authored keys and add interior map-knot samples with the existing runtime sampling semantics, while STEP tracks map only authored breakpoints. Binary64 calculations may narrow to finite binary32 track times, but distinct source or output instants that collide after narrowing refuse rather than being silently collapsed. A map knot and an authored key that are the same instant in that emitted binary32 domain — equal, or one representable place apart, since nothing is representable between them — are one emitted key at the authored time and value, carrying the control point's own output. FootCycleClipWarpKnotV1 is the single definition of that question, and time_warp_rows_v1 merges a track's authored keys with its knots into the key sequence a candidate stores. A knot-bearing row carries that knot — its control-point index and that point's two narrowed times — and no stored value, so the independent ClipMap proof in the CLI crate consumes the rows for the sequence and reads only the index, narrowing the control point itself rather than spelling coincidence a second way. What the proof takes from core is which keys exist and in what order; every number is its own, including a knot's two times, which it narrows again from the control point the row names rather than reading the producer's resolution of them. Tests freeze the row sequence and perturb producer times by one binary32 place in both directions: the proof rejects changed stored outputs and inserted-knot samples while still accepting the independently correct candidate. A coalesced row retains its authored value, so its source-time metadata is unused after row selection; perturbing that metadata alone remains accepted, while resampling a changed value is rejected. That holds for every authored key the map has a control point for, including a track's own first and last: the span test is inclusive of one place on either side, so which side of a rounding step the reconstructed instant lands on does not decide the outcome. A key the knot is bit-equal to binds before a key it is merely beside, and coalescing only ever re-times an authored key — it never removes one. The single exception is an interior point that is the same instant as a source endpoint, 0 or the duration: it contributes nothing, because the exact endpoint rows define the output there and a candidate retains those two instants as themselves. Identity maps return a structurally identical admissible clip.

CUBICSPLINE remains deliberately conservative: one-key tracks are retained; multi-key tracks are retained only when every stored key value is bit-exact equal and all stored input/output tangents are zero. Any other cubic track refuses the whole candidate. Finite quaternion keys must also have a finite positive binary32 squared length before any runtime sampling. Fixed checked caps refuse N+1 retained name bytes, tracks, authored keys/values, generated keys, and aggregate inspection work before candidate allocation. Candidate storage has a derived public upper bound: retained cubic storage is already bounded by authored values, while LINEAR/STEP emit one value per bounded generated key. This slice does not prove that the plan belongs to the loaded artifact, transform contact fragments, serialize or reread an output, or publish a generation directory. Those bindings and atomic transaction steps remain later #18 work.

F.10D Foot-cycle source preparation V1 (#18)

The CLI crate's next private seam binds the two prior pure boundaries to exact local source bytes without adding a command. It bounded-reads the manifest and parameterization once, resolves the exact member-reachable manifest source/config subset in canonical manifest source-key order plus every parameterization contact/output locator under its explicit declaration root, and rejects symlinks, canonical collisions, and platform-observable hardlink aliases before parsing any motion source. The future output leaf must be absent under an existing symlink-free parent. Pure manifest/runtime-set binding is validated first. Per-source and aggregate byte caps then cover only the exact member-reachable source subset, retained in manifest order; unrelated manifest sources do not gate this collection-scoped operation. Every reachable source and its exact config basis are loaded. Both the normalized config semantics and the explicit config byte identity are retained for later proof without a control-file reread. Any declared digest pin is enforced, while complete dependency-closure identities, complete raw take inventories, and exact index/name-to-normalized-clip witnesses are mandatory.

Each normalized reachable document passes the core's strict structural shape validation before it can reach sampling or candidate work. The adapter then preflights every selected member's frames * bones pose cells and frames * tracks sampling work, including the complete invocation totals, before constructing the first MetricGrids grid. It uses configured role resolution and root_trajectory_metrics as the existing authority. Horizontal admission is the binary64 hypot of retained signed X/Z endpoint components and yaw admission is the absolute value of retained signed accumulated unwrapped yaw. Missing, ambiguous, non-finite, or over-threshold evidence remains typed and makes the core planner refuse. The prepared collection retains the exact aggregate source pose-cell and sample-evaluation totals for a later shared source/output proof budget. The adapter cross-checks every plan's member, source artifact, closure, canonical fragment, selected take, duration, and root binding before building the pure clip candidate. Any later member failure discards all candidates; preparation does no filesystem mutation.

Contact output remains intentionally unfinished at this boundary. Core exposes one operation-aware handler that validates and reconstructs the exact closed stance-support detector payload for time_warp; the adapter retains that handler output together with the exact input fragment, artifact, closure, and operation. It cannot truthfully construct the output artifact identity or output dependency closure until a complete candidate document is serialized. The typed continuation accepts those later exact identities and only then calls transform_contact_fragment_v1. This slice therefore performs no serialization, generated-artifact hashing, reread/proof, generation staging, publication, or CLI exposure.

F.10E Bounded candidate GLB projection V1 (#18)

The next private format-edge seam is an in-memory GLB writer, not a collection producer. It first makes an allocation-free strict admission pass over the normalized document: checked row, name-byte, and traversal-work ceilings bound the modeled writer arrays, scalar binary components, validation lookups, source sidecars, and copied texture bytes before it constructs writer-owned JSON. It then counts the single shared projection with checked JSON/BIN/GLB framing arithmetic and returns an immutable receipt containing padded chunk sizes, framing size, and JSON/BIN digests. The receipt binds that emitted projection, not deliberately excluded source-only provenance. Retaining output reruns the projection and accepts only the same receipt, reserving exact output capacities before copying bytes.

The legacy path remains its established permissive conversion projection. The new StrictFootCycleV1 policy instead fails closed on any normalized fact that the writer cannot preserve: omitted clips/tracks, primitive or texture domains, non-explicit skinned inverse binds, invalid primary skin slots, incompatible skinned/unskinned mesh reuse, meshless materials, scene membership collapse, or source-local rest and source-skin root facts without an exact normalized representation. Strict output attaches a skinned mesh to its normalized node, rather than applying legacy's generated-holder normalization. Non-empty complete source material-resource sidecars refuse: V1 has no source texture/image identity or inspection-fact projection. An exact complete-empty resource graph is admitted only when the writer-facing material list is also empty. Source-only names, stable source indices, and image-inspection/provenance evidence remain deliberately outside the output projection; they are not presented as candidate facts. This seam does not bind a prepared collection member to its source, serialize contact transforms, reread/prove output, stage files, publish a directory, or add a CLI command.

F.10F Foot-cycle serialized-candidate proof V1 (#18)

The next private transaction consumes one complete prepared collection without adding filesystem or command behavior. For every declared member it clones the member's retained source document and replaces only the selected clip with the prepared candidate. It validates the document, runs StrictFootCycleV1 preflight, sums exact framed GLB sizes through one checked 256 MiB candidate ceiling, and admits the combined source-plus-output metric-grid pose-cell and sample-evaluation work before the first candidate write. All candidate writes finish before any readback; every exact byte vector is then reread through the ordinary glTF loader. Any refusal drops the local vectors and returns no partial batch.

The reread artifact identity must equal the exact retained bytes. Its dependency closure must be complete and bound to that primary identity, with no external resources and no reference target other than Primary; embedded BIN-buffer references therefore remain valid self-contained evidence. Only then does the transaction finish the prepared contact transform with the reread artifact, closure, and independently redetected producer identity.

Proof does not treat the existing checks as its verdict. It independently revalidates the finite strictly increasing endpoint-preserving map and exact LINEAR/STEP/cubic candidate relationship, including the constant-value, zero-tangent multi-key CUBICSPLINE restriction. One already-budgeted output grid per member supplies fresh contact topology/boundaries, gait phase/amplitude, signed Root/Hips trajectory, and per-bone loop pose plus linear/angular seam velocity. Contact and gait use the declaration's required proof fields; root facts are compared to retained signed source facts under the fixed 0.01 m and 1 degree V1 limits; loop facts use the exact captured source config's effective per-clip/check/default tolerance resolution. The result retains exact artifacts, closures, completed transforms, independently detected contacts, proof facts, phase spread, and separate source/output/combined work totals for the later evidence and publication transaction. Its contact proof independently repeats the per-side marker/disjointness validation, seam coalescing, logical-count and cyclic-onset checks, and deterministic true-boundary ordering rather than calling planner internals. Before its circular numeric error comparison, proof also requires the expected and independently detected (side, edge) signature in absolute phase-zero chronological order to match, so a complete left/right role swap remains a topology failure at every numeric error tolerance. It defines no public evidence schema, filesystem layout, publication, or CLI behavior.

F.10G Foot-cycle generation producer V1 (#18)

The final CLI-private transaction exposes exactly collection transform-foot-cycle COLLECTION.toml --parameterization FOOT-CYCLE.toml --format json; the required JSON-only format has no default and the parameterization is the sole destination authority. The glTF-only build retains this command, while FBX admission remains feature gated. Every successful source becomes a self-contained strict GLB.

For N members the fixed generation has checked 3N+1 cardinality and exact declared order: each members/NNNNNN/ contains artifact.glb, contact-fragment.json, and evidence.json, followed by the root aggregate-evidence.json. The member evidence V1 contract is urn:animsmith:schema:foot-cycle-member-evidence:1; it binds exact control inputs, source artifact/config/complete closure, output artifact/complete closure, transformed and independently redetected contacts, canonical map identity, policy, proof facts, and deterministic relative paths. Aggregate evidence V1 is urn:animsmith:schema:foot-cycle-aggregate-evidence:1; it binds the ordered member summaries and member-evidence identities, phase spread, component and exact total bytes, retained GLB bytes, and source/output/combined proof work. Strict bounded readers and published JSON Schemas close both records.

All GLB, fragment, member-evidence, and aggregate-evidence bytes share one 256 MiB retained/staged ceiling; each component retains its narrower contract cap. Checked arithmetic admits the exact file count and byte total, including the aggregate record's own converged serialized size, before the first filesystem write. One publish_generation call stages and flushes the complete fixed set under a sibling directory, then performs one platform no-replace directory rename. An existing or racing destination wins unchanged. This is atomic visibility of a new local generation, not replacement, power-loss, network filesystem, or crash-cleanup atomicity.

Success emits the exact aggregate file bytes to stdout only after publication; a broken stream is best-effort diagnosed and retains exit 0. Valid asset, capability, refinement, contact-transform, or proof refusal publishes nothing, emits one producer-refusal:1 record, and exits 1. Malformed or stale control bindings, unsafe/aliased paths, I/O, serialization, publication, and race failures publish nothing, keep stdout empty, and exit 2. The producer never rereads mutable manifest, parameterization, config, or contact controls after their exact capture and never substitutes the writer's candidate model for the independent serialized readback proof.

F.11 Transition-family declaration V1 (#148)

Issue #148 freezes declarations for transition families without implementing transition checks, reports, inferred graph edges, or runtime generation. Its independent identity is:

urn:animsmith:schema:transition-family:1

The document-local tables are admitted by the CLI's normal strict config loader, which retains the whole-config exact input identity alongside the normalized declaration. The JSON-only document command and manifest-bound collection command now evaluate that authority. The collection form binds its exact manifest before source work and reloads only manifest-witnessed takes.

The format-neutral core now owns the strict SkeletonBasisV1 identity, both document and collection transition-pose evaluators, and their shared result contract urn:animsmith:schema:transition-pose-evaluation:1. It admits a mutable loaded document's skeleton and selected T/R tracks, plans bounded endpoint work before sampling, and returns an immutable complete/finding/not-evaluated result. The collection adapter reloads only manifest-witnessed sources; neither adapter adds a lint check. The document adapter is the JSON-only animsmith evaluate-transition-poses INPUT --format json command: it loads one document through the normal public loader/config path, binds the loaded exact raw document identity, and emits the core result unchanged. It uses the exact zero-byte TOML sequence as the defined omitted-config declaration source, so an omitted config and an explicitly empty config have the same exact and normalized declaration identities. The collection adapter preserves every declared family member and makes an unavailable or unusable source a whole-family incomplete result rather than evaluating a subset. Its scope-neutral subject_input binds the exact raw document in this document evaluator; the collection adapter binds its manifest subject under the same result schema. The normalized declaration scope determines which subject meaning applies. For document scope, the public evaluator accepts the loader's same-load DependencyClosureV1, takes subject_input only from its primary_input, and retains its complete subject_dependency_closure_identity. Every document member retains the same primary identity as source_input and the same complete closure identity as source_dependency_closure_identity. Complete coverage and a present DependencyClosureIdentityV1 are both required before any configured family can be complete; partial, unavailable, or identity-less coverage makes every configured family incomplete/not_evaluated with dependency_closure_incomplete. The no_configured_families result is the deliberate exception because it performs no source evaluation and remains a complete pass even when closure capture is unavailable.

Collection scope does not fabricate a dependency closure for its manifest: subject_input is the manifest's primary identity, while each available member independently retains its raw source_input and complete source_dependency_closure_identity. An unavailable source may retain a null source_input when no primary bytes were obtained. One missing or incomplete member closure makes the entire family dependency_closure_incomplete, but other available members keep their closure identities as positive evidence; the family is never evaluated as a survivor subset. Thus unchanged primary text glTF bytes cannot mask changed external animation-buffer bytes. Before sampling, the evaluator directly verifies every declared index/name witness, admits at most 4,096 document clips for its one-pass global duplicate-name proof, 4,096 bones, and 8 MiB of basis-name text. A document above the clip cap with no direct witness contradiction yields normal input_limit family rows without scanning its full name domain. Each selected clip first admits at most three raw flat track rows per bone (12,288 maximum), then bounds the consumed translation/rotation domain to two channels per bone. Scale values, shape, and duplicates remain semantically ignored, but raw scale rows count toward that first resource admission because the loader stores all properties in one vector. Across the invocation it inspects at most the V1 aggregate-comparison bound of 16,777,216 selected time/value elements. A valid-witness input exceeding one of these admission limits retains every family as incomplete/not_evaluated with input_limit; unrelated assets and unselected clips are outside this evaluator boundary. Before sampling it serializes the full pair-free authority, including subject and per-member dependency-closure identities, and subtracts that from a conservative detailed JCS reservation, producing result_limit rather than cloning a representation that must later be discarded as over-cap.

The declaration has a tagged scope, a stable family_id, an explicit ordered members array, a boundary (entry, exit, or both), a typed basis, and typed named tolerances. A family has at least two members. family_id is unique within its owner and is never inferred from filenames, directories, embedded names, or member order. Unknown fields and duplicate members are invalid.

Document-local family ids are one lowercase-ASCII token, 1–255 bytes, starting with [a-z0-9] and continuing with [a-z0-9._-]. The table key is the id and there is no duplicate family_id field. TOML must quote the key whenever it contains punctuation or a dot (the canonical spelling quotes it always), for example [transition_families."walk_to_run"] or [transition_families."combat.entry.v1"]. Collection family ids retain Appendix F's slash-qualified logical-id grammar and collection-id prefix.

The two scopes are intentionally different authorities:

  • document declarations are placed in the existing animsmith.toml config basis under [transition_families."<family_id>"]. The table key itself is the family id; it is a reusable config declaration and carries no artifact digest. The document evaluator binds the exact primary document InputIdentity and complete dependency-closure identity in its output, while members use exact embedded take-index/name witnesses. A repeated or loader-ambiguous take name is a strict resolution failure.
  • collection declarations use a separate declaration envelope, not an extension of collection-manifest V1 and not another path/member authority. The envelope binds the exact collection-manifest InputIdentity {sha256, bytes}, collection id, and one or more family records. Each record references a declared logical clip id plus its corresponding source, take-index, and take-name witness.

A declaration cannot mix scopes, use a filesystem path, use an animation-array index as identity, or import a member from another document or collection. Missing, duplicate, cross-scope, and ambiguous members are strict resolution failures. stale_digest is a collection-only failure: it means the envelope's manifest InputIdentity no longer matches the declaration. A source digest pin, when present in that manifest, is enforced by manifest resolution and a mismatch makes the affected member unavailable; it is not a second identity carried by this declaration. A reusable document-local config has no stale-digest state at parse time; the document evaluator binds the exact primary document InputIdentity plus its complete dependency-closure identity and reports its source/take resolution in its output. A family is not silently reduced to the members that happen to resolve, and a collection family is not copied into a document-local config. The existing [clips], [gait_groups], and [sync_groups] sections retain their document-local embedded-name semantics; this declaration does not replace, reinterpret, or create a second authority for them. The document-local table and collection envelope share the same transition-family:1 family-record semantics; only their ownership and placement differ.

V1's basis is explicit and engine-neutral: translation is metres in the declared skeleton-local basis, rotation is degrees in the same basis, and time is normalized clip time in [0, 1]. The named tolerance record is closed and unit-bearing: translation_m, rotation_deg, and time_normalized, each a finite non-negative value. boundary selects whether the evaluator considers entry, exit, or both; it does not itself perform a comparison. Additional policy fields require a separately versioned contract, not a generic TOML map or untyped extension payload.

Both declaration placements are bounded before retention or canonicalization. The exact declaration source is at most 8,388,608 bytes and 16 nested tables/arrays, with at most 4,096 families, 4,096 members in one family, and 16,384 members across all families. Authored strings are at most 4,096 UTF-8 bytes, with the 255-byte identifier limits above applied more strictly. The normalized JSON envelope is at most 8,388,608 JCS bytes and has the same nesting limit. Depth counts each table/array or JSON object/array on the root-to-value path, inclusive; the root table or envelope object is depth

  1. The strict reader accepts each exact maximum and rejects the first byte, element, string byte, aggregate member, or nesting level above it while decoding, before retaining that excess value or allocating an unbounded canonical buffer. Canonical byte limits are enforced with a bounded JCS sink.

For example, a collection-owned declaration can be written as the following typed TOML-like value. The logical id and source/take witnesses must agree with the collection manifest; they are repeated here to make a stale or reordered member explicit rather than silently resolving by name:

schema = "urn:animsmith:schema:transition-family:1"
schema_version = 1
scope = "collection"
collection_id = "com.example.pack"
manifest_input_identity = { sha256 = "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", bytes = 9876 }

[[families]]
family_id = "com.example.pack/transitions/walk-to-run"
boundary = "both"

[families.basis]
translation = "skeleton-local-metres"
rotation = "skeleton-local-degrees"
time = "normalized-clip"

[families.tolerances]
translation_m = 0.05
rotation_deg = 5.0
time_normalized = 0.02

[[families.members]]
logical_id = "com.example.pack/locomotion/walk-forward-in-place"
source = "walk-forward"
take_index = 0
take_name = "Take 001"

[[families.members]]
logical_id = "com.example.pack/locomotion/run-forward-in-place"
source = "run-forward"
take_index = 0
take_name = "Take 001"

The document-local placement uses the existing config basis and the same family-record fields:

[transition_families."walk_to_run"]
schema = "urn:animsmith:schema:transition-family:1"
schema_version = 1
scope = "document"
boundary = "entry"

[transition_families."walk_to_run".basis]
translation = "skeleton-local-metres"
rotation = "skeleton-local-degrees"
time = "normalized-clip"

[transition_families."walk_to_run".tolerances]
translation_m = 0.05
rotation_deg = 5.0
time_normalized = 0.02

[[transition_families."walk_to_run".members]]
take_index = 0
take_name = "Walk"

[[transition_families."walk_to_run".members]]
take_index = 1
take_name = "Run"

After strict validation, the normalized JSON representation has exactly one of the following two closed envelope shapes. The collection form is:

{
  "schema": "urn:animsmith:schema:transition-family:1",
  "schema_version": 1,
  "scope": "collection",
  "collection_id": "com.example.pack",
  "manifest_input_identity": {"sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789", "bytes": 9876},
  "families": [{
    "family_id": "com.example.pack/transitions/walk-to-run",
    "boundary": "both",
    "basis": {"translation": "skeleton-local-metres", "rotation": "skeleton-local-degrees", "time": "normalized-clip"},
    "tolerances": {"translation_m": 0.05, "rotation_deg": 5.0, "time_normalized": 0.02},
    "members": [
      {"logical_id": "com.example.pack/locomotion/walk-forward-in-place", "source": "walk-forward", "take_index": 0, "take_name": "Take 001"},
      {"logical_id": "com.example.pack/locomotion/run-forward-in-place", "source": "run-forward", "take_index": 0, "take_name": "Take 001"}
    ]
  }]
}

The document form lifts the repeated table contract fields into one envelope; the table key becomes family_id:

{
  "schema": "urn:animsmith:schema:transition-family:1",
  "schema_version": 1,
  "scope": "document",
  "families": [{
    "family_id": "walk_to_run",
    "boundary": "entry",
    "basis": {"translation": "skeleton-local-metres", "rotation": "skeleton-local-degrees", "time": "normalized-clip"},
    "tolerances": {"translation_m": 0.05, "rotation_deg": 5.0, "time_normalized": 0.02},
    "members": [{"take_index": 0, "take_name": "Walk"}, {"take_index": 1, "take_name": "Run"}]
  }]
}

No owner or source-input identity is injected into the document form. A V1 evaluation record binds three distinct facts: the exact declaration-source InputIdentity (the whole config for document scope or the declaration TOML for collection scope), the normalized declaration InputIdentity computed over the JCS bytes above, and the evaluated document or collection-manifest InputIdentity. The collection form additionally retains its embedded exact manifest binding. These identities are never interchangeable.

The strict reader rejects malformed/duplicate declaration members, and the document evaluator rejects missing or ambiguous document witnesses before it emits a result. The collection adapter additionally rejects stale or cross-scope members. The collection envelope's manifest digest/bytes binding makes a manifest reorder or edit stale even when its collection id and logical ids are unchanged.

Canonical serialization first sorts decoded family declarations by their stable family_id and preserves declared member order, then serializes that typed JSON representation with RFC 8785 JCS. The exact source TOML InputIdentity remains a separate binding and is never replaced by the normalized JSON identity. The strict reader validates owner/scope, member syntax, basis, units, finite tolerances, and canonicalization before a declaration is accepted. The document and collection commands produce V1 findings/reports; required gameplay metadata, inferred edges, state machines, blend trees, and runtime transition generation remain follow-up work.

F.12 Directional-speed policy/evaluation V1 (#552)

The directional-speed policy is a separate, manifest-bound declaration for directional-blend runtime sets. It is not a field in collection-manifest V1, does not revise collection-output V2, and does not infer membership from filenames, paths, engine controller types, or other host metadata. The typed contract and strict TOML reader feed the evaluator and its JSON-only CLI consumer; no new manifest or collection-output authority is introduced.

The schema identity is urn:animsmith:schema:collection-directional-speed-policy:1, with schema_version = 1. Its wire envelope has exactly these top-level fields: schema, schema_version, manifest, runtime_set_id, source_basis, diagonal_behavior, direction_tolerance_deg, mode, the mode-specific fields, and members. manifest has exactly the collection-manifest V1 schema/version, collection_id, and input = { sha256, bytes }; that input identity is the exact manifest identity, not a digest of normalized values. runtime_set_id is a logical id that must bind to a manifest runtime set of kind directional-blend.

The explicit source_basis contains x and z, each a semantic 2-D orientation witness for the raw collection-output V3 +X/+Z endpoint displacement. Their magnitudes are nonsemantic; the evaluator uses unit axes for heading comparison. Every component is finite and has absolute value at most 1,000,000. Each axis is nonzero. Perpendicularity is judged by the normalized dot product

abs(dot(x / ||x||, z / ||z||)) <= 1e-9.

The normalized calculation uses hypot lengths and component-wise division, so its decision is independent of axis scale and does not square tiny values or overflow large bounded values. The 1e-9 cosine threshold is the frozen COLLECTION_DIRECTIONAL_SPEED_POLICY_V1_MAX_AXIS_COSINE bound. It rejects near-parallel and diagonal axes, including tiny vectors, while accepting orthogonal subnormal-scale axes when their finite representation has a nonzero length.

members is an ordered sequence. Each member has exactly one logical id and one semantic coordinate = [x, z]; it may additionally carry speed_mps only for authored mode or expected_ratio only for ratios mode. Coordinates are finite, bounded by the same component limit, nonzero by component value, and unique by exact finite bit identity after canonicalizing both signed zeros to one zero key. The reader preserves declared order. A consumer binds the policy only when the manifest identity, runtime-set id, runtime-set kind, and complete member sequence all match exactly; missing, extra, or reordered members are binding errors.

The closed diagonal_behavior tokens are preserve and normalize. The existing speed_mps and expected_ratio fields are unit-input/base targets. For a semantic coordinate c, preserve defines gain g(c) = hypot(c) and normalize defines gain g(c) = 1. Therefore uniform expected speed is base * g(c), authored expected speed is the member base speed_mps * g(c), and ratios compare expected_measured_ratio_i_to_ref = declared_expected_ratio_i * g(c_i) / g(c_ref), with the reference declaration fixed at 1.0. This gain does not affect direction comparison. The declaration carries finite non-negative direction_tolerance_deg, bounded inclusively by 180 degrees. The closed speed modes are:

  • uniform: requires finite non-negative bounded uniform_speed_mps and speed_tolerance_mps; member speed and ratio fields are forbidden.
  • authored: requires finite non-negative bounded speed_tolerance_mps and exactly one finite non-negative bounded speed_mps per member; uniform and ratio fields are forbidden.
  • ratios: requires finite non-negative bounded ratio_tolerance, a reference_member, and exactly one finite non-negative bounded expected_ratio per member. The reference member must be present and its expected ratio must be exactly 1.0; speed fields are forbidden.

All scalar bounds are inclusive. Unknown fields, duplicate TOML keys, unknown mode or diagonal tokens, missing or mode-inapplicable fields, malformed lowercase SHA-256 identities, invalid manifest schema/version, non-finite numbers, out-of-range components, invalid bases, invalid member coordinates, duplicate member ids, and duplicate coordinates are strict control errors. The source reader is capped at 8,388,608 bytes and 4,096 members; each exact limit is accepted and the first excess member is rejected before it is retained. The reader first decodes only schema and schema_version so unsupported declarations receive stable unsupported-schema or unsupported-version classifications; it then performs the complete deny-unknown-fields decode and typed invariant validation. This two-pass lifecycle is classification, not a second policy authority.

There is no evaluator inference or fallback. The pure, format-neutral collection-directional-speed-evaluation:1 result and typed adapter consume already strictly decoded collection-output V3 evidence; they do not revise V3. The immutable result binds the raw policy TOML and evidence JSON InputIdentity values as well as the manifest, and retains every member's coordinate, raw root-travel values, projected heading, comparison values, pass/violation state, lifecycle, and gaps.

For complete evidence, finite endpoint X/Z displacement is normalized before the unit source X/Z axes form its semantic heading. The declared coordinate is also normalized; the included angle is atan2(abs(cross), dot) in degrees. Speed uses published speed_mps, never travel. Every tolerance is inclusive, so a finding requires strict excess. A zero endpoint, zero ratio reference, or unrepresentable derived product/ratio yields a complete typed not-evaluated result (zero_net_displacement, zero_reference_speed, or numeric_range) without a subset or fabricated finding. Preserve uses hypot(coordinate) as the expected-speed gain; normalize uses one, and the ratio reference remains exactly one. Findings retain manifest order, direction before speed/ratio.

animsmith collection evaluate-directional-speed --policy POLICY.toml --evidence COLLECTION-OUTPUT.json --format json captures policy bytes under the 8 MiB cap and evidence bytes under the 256 MiB cap before retaining them, invokes the strict readers/adapter/evaluator, and serializes the result once to stdout. Invalid, stale, wrong-kind, unreadable, malformed, or over-budget control inputs exit 2 with no result. Incomplete/not-evaluable evidence and policy findings emit the result and exit 1; only a complete passing policy exits 0. There is no filesystem publication, controller generation, retiming, stride rewriting, or universal speed rule.

Scale proof calibration

This document owns the reproducible calibration populations, measured demand, historical policy comparisons, and machine-local timing notes behind ScaleTolerancePolicy::APPENDIX_D_V6. The normative operation, tolerance, provenance, work-budget, proof, and publication contracts remain in DESIGN.md Appendix D.

The calibration is evidence for the fixed policy, not another policy authority. Changing an accepted bound, work budget, magnitude definition, sampling rule, or proof comparison requires a new policy identity and corresponding normative design change; editing the measurements here cannot change runtime behavior.

Rounding calibration sweep

4 is measured, not assumed, and the measurement is checked in: calibrate_f32_rounding_ulps in crates/animsmith-core/src/scale/tests.rs. Run it with

cargo test -p animsmith-core --release --lib \
    calibrate_f32_rounding_ulps -- --ignored --nocapture

It builds and proves 360_000 correct candidates over 144 cells: the cross product of nine operations (rest/bind at root scale 3190, and whole-document conversion at {1e-4, 0.01, 0.1, 1.5, 7.3, 100, 3190, 1e6} — both directions of the factor), four slot compositions (analytic binds, where abs(W * B) is 1, and composed slots at abs(W * B) = {1e-3, 1, 1e3}), two blends (two slots that oppose on the swept vertex and cancel it to the origin under balanced weights, and two independent slots that do not), and two explicit weight profiles. The balanced profile uses [0.5, 0.5]. In the mismatched profile, every vertex whose two production influence bases differ gives the larger base a log-uniform weight in [1e-20, 1e-2] and the smaller base weight 1. The measured population contains 274_670 such vertices — 74_085 with slot 0 larger and 200_585 with slot 1 larger — and the sweep asserts strong nonzero floors for both orientations rather than merely counting profile labels. Each cell draws 2500 candidates. Joint locals and vertex positions are drawn log-uniformly over six and eight decades in random directions, every joint carries a random rotation, and half of every cell's trials carry a parent chain that cancels. The generator is a written-out SplitMix64 whose seed is derived from each cell's own coordinates — conversion, composition, blend and weight profile — rather than from its position in the loop, so a cell's population does not move when a neighbouring cell is added, removed or reordered.

The bit stream is identical on every machine and every run; the rigs drawn from it are not quite. The draws run f64::powf for the magnitude decades, f64::sin/cos for the directions, and glam's f32 sine and cosine for the rotations, none of which any platform is required to round identically. Two machines can therefore differ in the last ulp of a joint local and in the last printed digit of the worst demand. The table below records the reference run for this policy revision; the checked test asserts platform-tolerant non-silence floors and the strict < 4 safety ceiling rather than pretending those libm-dependent last digits are normative.

quantityworst over the population
skinned bounds2.768
the skin equation2.213
rest translation2.583
refused correct candidates0 of 360_000

The quantity is residual / (magnitude * 2^-23): the raw ulp count, not net of the scalar band that is paid first. It therefore overstates what the count is actually asked for, by the whole scalar band, and a worst case under 4 measured this way is a worst case under 4 however the two terms are split. An earlier revision of this section reported the net demand instead, from populations that were never checked in and so could not be re-derived; the figures above replace them.

The 144-cell phase remains intentionally shallow. A separate deep-chain phase proves 80 correct animated candidates at depths 8, 16, 32, 64, 128, 192, 256, 512. At every depth it covers rest/bind and the eight whole-document conversions; the whole-document cases use the literal 170-degree chain, while rest/bind uses an exact half turn because repeating the approximate quaternion eventually leaves the operation's supported affine-input class. Eight additional whole-document cases use a literal step that closes a 192-link ring. In test builds the central production comparison records each raw ULP demand at the exact point its residual and provenance meet. Both shallow and deep calibration fold those private maxima; the deep phase also folds production-owned counts. The released proof layout and runtime path are unchanged, and calibration no longer rebuilds a parallel pose/slot/bounds walk or divides unrelated obligation-wide maxima.

deep-chain quantityworst over 80 casescomparisons
rest translation0.71512_488
sampled trajectory0.71524_976
the skin equation0.143240
skinned bounds0.1491_440
refused correct candidates0—

The harness retains and prints a separate record for every declared depth, rather than folding the individual demands immediately into the aggregate above. The depth-192 row also includes the eight ring cases; every other row contains rest/bind plus the eight whole-document conversions.

depthresttrajectoryskin equationboundscomparisons (rest / trajectory / skin / bounds)
80.5780.5780.1430.14981 / 162 / 27 / 162
160.5780.5780.0670.073153 / 306 / 27 / 162
320.5780.5780.0760.078297 / 594 / 27 / 162
640.5780.5780.0630.063585 / 1_170 / 27 / 162
1280.5780.5780.0370.0391_161 / 2_322 / 27 / 162
1920.7150.7150.0340.0333_281 / 6_562 / 51 / 306
2560.5780.5780.0340.0342_313 / 4_626 / 27 / 162
5120.5780.5780.0190.0194_617 / 9_234 / 27 / 162

An unaffected instance's binds demand 0, and always will: a correctly built candidate leaves both effective matrices equal, including when one side stores an explicit identity and the other uses the format-defined identity default.

4 is the next power of two above every figure above, measured over these declared populations. It is not an analytic bound. An earlier revision reasoned from one four-term matrix inner product; v4 exposed the missing dimension because one composition happens per link. Its maximum over links stayed flat while coherent rounding accumulated with depth, and correctly built candidates began refusing at depth 180.

Historical v5 calibration of the recurrence retained by v6. The v5 policy replaced the depth-flat maximum with the additive provenance recurrence that v6 still ships. Its exact formula, scope, and fixed-stage semantics have one normative owner in DESIGN.md Appendix D. This note retains the evidence for that choice. A 512-link zero-translation chain adds nothing below a translated root; the corresponding chain of underflowed nonzero translations carries only its vanishing contribution instead of charging the million-unit parent 512 times. An uneven chain accrues its actual per-link bases rather than depth * max, and the literal 192-link ring plus the 170-degree chain prove through depth 512. The checked-in shallow and deep calibration and the isolated floors below delimit the empirical envelope retained by v6.

A residual above the count is evidence about the magnitude before it is evidence about the count. Three revisions have now found the magnitude wrong rather than the count too small — the skinned extent alone, which missed the W * B composition; abs(W) * abs(B) alone, which missed what the parent chain had already cancelled; and abs(p) alone, which missed the other factor of the transform it was named for — and in all three the measured excess was hundreds or hundreds of thousands of ulps, not a factor of two. Each was invisible because the calibration population could not express the shape: no rig in it composed a slot to anything but the identity. That is what the checked-in sweep is for, and why its cells name the shapes rather than only the magnitudes.

The cost is real and it is not bounded. The four chain-dominant fixtures search their declared positive-binary32 intervals in the normal suite, sample 4097 evenly spaced bit coordinates as a gross non-monotonicity guard, and pin the adjacent accepted/refused transition found by bisection plus the exact refused residual/tolerance bits. This is not an exhaustive monotonicity proof. The four searches remain comfortably sub-second in the debug reference run, so a second ignored-only search path would add structure without buying practical test time. The v4 values are frozen historical measurements from the exact merged commit 0a253228dc2d557a9030cfd72f2b15326f4853bd, independently reproduced during audit with the same fixtures and mutation axes. The v5 suite compares its production endpoints with those recorded bits; it does not carry a second implementation of the retired policy.

obligation / mutationv4 refused transitionv5 adjacent accepted / refusedv5 refused-endpoint observed / tolerance
RestTranslation, unskinned rest x3.1875 (0x404c0000)4.5625 / 4.5625005 (0x40920000 / 0x40920001)4.5686544 / 4.5633805
Trajectory, both unskinned key x values3.1875 (0x404c0000)4.5625 / 4.5625005 (0x40920000 / 0x40920001)4.5686544 / 4.5633805
SkinMatrix, cancelling-joint inverse-bind x3.5713947 (0x406491bb)5.3570914 / 5.357092 (0x40ab6d4b / 0x40ab6d4c)4.5633788 / 4.5633783
Bounds, chain-dominant point y2.9826486 (0x403ee3b7)4.503774 / 4.5037746 (0x40901eeb / 0x40901eec)4.5633788 / 4.5633786

Those are fixture-local transitions, not global smallest defects. The typed refused endpoint isolates the named obligation: the rest/trajectory bone is not skinned, the inverse-bind mutation reaches SkinMatrix before Bounds, and the million-unit x coordinate gives MeshPosition a wider band than the Bounds y mutation. Reverting the recurrence to v4's maximum reproduces the recorded v4 boundary and fails the strict v5 comparison.

The general cost remains unbounded relative to the compared quantity. 4 * 2^-23 is 4.77e-7 of the magnitude the arithmetic ran on, which is 4.77e-7 of the compared quantity only when the two coincide and is otherwise 4.77e-7 * (operand magnitude / compared magnitude) of it. That ratio has no upper bound, and it is exactly the ratio the term exists to cover, so the term and the compared quantity diverge precisely as far as the operand/result cancellation documented above.

Concretely, on the far-joint rig of a_joint_far_from_the_geometry_it_carries_still_proves_its_bounds — joints 3.2e6 from the origin carrying geometry within one unit of themselves, so W * B is near-identity — the term is 4.44 against a compared magnitude of 1.0, and the largest inverse-bind x shift still accepted is 4.09375 units — the smallest refused is the next binary32 above it. A regenerated bind wrong by four units is accepted there. The bracket is pinned by the_far_joint_rig_admits_a_four_unit_bind_shift_and_refuses_the_next_one_up, because a floor quoted in prose and nowhere held to drifts: an earlier revision of this section stated 4.09, which is on the accepted side of the real floor and so described a bracket that does not exist. As a rule: for a rig whose joints sit k times further from the origin than the geometry they carry, SkinMatrix and Bounds lose discriminating power in proportion to k.

Folding the parent chain into the magnitude does not move that number: on the far-joint rig abs(W) * abs(B) already reads 6.4e6 against the chain's 3.2e6, so the max is unchanged and the floor is still 4.09375 units. The chain widens the magnitude only where a chain actually cancelled, always in proportion to what it cancelled, and leaves it untouched everywhere else. Buying the same admissions by raising the count instead would have cost the whole factor on every slot, including the ones that lost nothing, which is why the correction is to the magnitude and the count stays at 4. That is the general rule this section applies each time: a_parent_chain_whose_translations_cancel_still_proves_its_skin needs 524288 ulps of the base without the chain and 0.08 of it with, and no count between those two is a policy anyone could defend.

This is a property of the f32 inputs, not of the policy, and precision does not fix it. The dominant error is not the composition's own rounding. It is the stored inverse bind's translation column — accurate only to its own ulp — amplified by W's linear part into a product that cancellation has made near-identity. Composing W * B in f64 from the same f32 stored values was measured over a 30_000-candidate rest/bind population: it moves the worst skin residual from 2.50 to 2.06 ulps and the worst bounds residual from 1.68 to 0.90, an improvement of under a factor of two, and leaves the worst residual at 86 % of the compared product's own magnitude against a 1e-5 relative band. On the far-joint rig it moves the floor from 4.09375 units to about 4.16 at the same four ulps, or about 3.16 at the three ulps that residual would then permit — those two are approximate, being measurements of a composition this tree does not perform and so not pinned by any test here. The term covers a quantization the file itself already performed, and no amount of proof-side precision can undo it.

What the term does buy is the direction that matters: no obligation is held to a band tighter than the rounding its own arithmetic genuinely incurs, which none of them was ever sound doing. The converse — that each obligation refuses every error larger than its rounding — is a stronger claim than anything here establishes. What is pinned is one adjacent bracket per named mutation axis, including the far-joint inverse-bind bracket at 4.09375 accepted and the next binary32 value refused.

Transform-only rounding and overflow/refusal semantics are current policy, not calibration conclusions, and therefore live only in DESIGN.md Appendix D. Their checked evidence includes a_rig_whose_skinned_extent_passes_the_square_root_of_f32_max_still_proves, a_rig_whose_composition_operands_overflow_f32_still_proves, and a_parent_chain_whose_operand_sums_overflow_f32_still_proves.

Sampling-budget timing history

4e8 is a wall-time ceiling expressed in work units. What that ceiling costs in seconds is a property of the machine, not of this design. Historical v3 measurements were linear in the charge across four doublings of the budget in both shapes, which is the evidence that the charge is a proxy for real work rather than an arbitrary count. Both shapes are fully specified here, so a reader can rebuild them and measure their own machine: the slot-dominated one is 200 instances of a 99-joint skin list with one vertex each, and the vertex-dominated one is a single instance of that skin list with a 10_000-vertex primitive, each with as many sample times as 4e8 admits, and every vertex in both carries exactly one non-zero influence.

The released arithmetic also gives reproducible sizing landmarks independent of machine timing. Appendix D's 200-bone, 100k-vertex example costs 201_000 units per sample, so 4e8 admits 1,990 samples; the same rig with 10k vertices costs 21_000 and admits 19,047. A 100k-sample clip on the first rig asks for 2.01e10 units and is refused. Its 30-second, 30-fps example costs 180_900_000, leaving about 2.2x headroom, or roughly 66 seconds at 30 fps for that exact shape. These are consequences of the released formula, not wall-time measurements.

The one-influence condition above is part of the shape and not an incidental detail. The budget charges per vertex, but the f32-rounding term's stage-1 work runs once per non-zero influence — the skinning loop continues on a zero weight — so a rig whose vertices carry four influences pays that stage four times per vertex against the same charge. The seconds below therefore describe a one-influence shape and understate a four-influence one; the work units are identical for both. Appendix D §D.1 names that vertex-versus-influence discrepancy as part of the normative work-budget definition so a rebuilder does not have to infer it from this historical timing population.

On one developer machine under v3 the slot-dominated shape at the ceiling measured 6.7s and the vertex-dominated one 3.3s, a ratio of 2.0. Neither number is a bound this design guarantees or a v4 measurement, and an earlier revision of this section claimed one — "stays inside five seconds" — against a measurement taken on different hardware and a different tree. The ratio moved too: before f32_rounding_ulps the same machine measured 7.1s and 2.0s, a ratio of 3.5. The absolute seconds are what a reader cannot check; the linearity and the ordering are what they can.

Historical v3 cost of the f32-rounding term. Deriving each obligation's magnitude was per-slot and per-vertex work that the tree before appendix-d-v3 did not do, and the vertex-dominated shape is where it showed. Two baselines are quoted here and they are not the same baseline, so both are named. Against no rounding term at all — the appendix-d-v2 tree — on one machine, over twelve runs of each shape and taking minima, that shape went from 2.02s to 3.31s, +64 %, for the same 1_932_084 skin and 117_096 bounds comparisons.

Against the term with stage 1 read as abs(p) — the form this design carried mid-revision, before stage 1 was corrected to the transform's operand product — the correction alone costs +27 %. Measured in one session on the same shape and machine, minimum of five runs: 2.57s with no stage-1 term at all, 2.87s reading position.length(), 3.64s reading column_operand_magnitude. That is +12 % for having any stage-1 term, +27 % for correcting it, and +42 % for both together. The absolute seconds differ from the paragraph above because they are a different session of the same shape on the same machine; only ratios within one session are comparable.

Within the first v3 baseline, the cost was dominated by taking the length of each skinned position, 390 million of them at the ceiling. Widening that length unconditionally to f64 cost +83 % instead; making the f64 square a fallback behind a finite f32 one recovers about a quarter of the regression and none of the correctness, since the fallback is still what admits an extent past sqrt(f32::MAX). The remaining +64 % is the f32 square root itself, which is work this obligation did not previously do at all.

The slot-dominated shape did not regress in that v3 session: 7.10s before the term and 6.70s after. Folding the parent chain into the magnitude added nothing measurable on either shape — one Mat4 * Vec4 per bone per document side per sample time, accumulated in the walk that already composes the world matrices, against work that is per slot and per vertex.

Those timings are retained as history, not carried forward as v4 evidence. v4 removes the per-vertex L2/square-root stage and widens its weighted numerator and denominator to binary64, so the old cost attribution and which shape is slowest may have changed. The budget is nevertheless unchanged: it bounds the same proof passes and cardinalities, still admits the production-sized example in Appendix D, and remains a conservative resource limit. This design makes no v4 wall-time or shape-ordering claim without a v4 benchmark.

Game-Ready Animation Clips: Research Notes for Animsmith

Date: 2026-07-09

Scope note: this is a dated research artifact, not the current animsmith product contract. The user-facing explainer of what animsmith checks and why is docs/game-ready-clips.md. Before converting any recommendation into an issue, reconcile it against DESIGN.md, the shipped CLI docs, the stable check ids, and any newer documentation that has merged since this note was written.

Executive Summary

A game-ready animation clip is not just an animation that looks good in a DCC tool. It is a clip whose data, rig assumptions, root motion behavior, loop boundaries, contact quality, metadata, compression tolerance, and import settings are predictable enough for a real-time engine and a production pipeline.

The engine documentation points to the same practical contract from different angles:

  • Unity treats clips as isolated units of motion, then exposes import-time decisions for clip ranges, looping, root transform rotation and position, compression, custom curves, events, masks, and retargeting warnings.
  • Unreal treats an Animation Sequence as skeletal transform data tied to a Skeleton, with import choices for frame range, sample rate, skeleton matching, custom attributes, root motion, notifies/curves, and compression.
  • Godot exposes importer-level decisions for animation optimization, saving animations as separate files, timeline slicing, and retargeting through bone maps/rest-pose alignment.
  • Bevy is glTF-forward and code-driven: clips commonly arrive as named glTF animations, are loaded through asset labels, and become AnimationClips driven by AnimationPlayer, AnimationGraph, transitions, events, masks, and target IDs.
  • glTF constrains animation data at the format level: node hierarchies must be valid trees, animated nodes must use TRS rather than matrix transforms, times and accessors must be valid, and interpolation must be supported correctly.

That suggests animsmith's core value proposition:

Animsmith should be a repeatable animation-readiness gate and repair assistant: it turns ambiguous "this animation feels broken in the engine" feedback into measurable checks, actionable reports, CI-friendly failures, and safe transforms for common engine import workflows.

The best first product surface is not a full DCC replacement. It is a clip audit, measurement, normalization, and reporting tool for teams that receive raw mocap, marketplace assets, outsourced FBX files, glTF characters, or DCC exports and need them to become engine-safe assets.

Current Animsmith Reconciliation

This research was written from engine and pipeline needs inward. The existing animsmith implementation already covers part of that surface. The useful planning question is therefore not "what should animsmith build from scratch?" but "which gaps remain after mapping the research against the current product?"

Shipped or already documented capabilities include:

  • animsmith lint as the CI-friendly audit command, with project config, stable exit codes, text output, and JSON output.
  • animsmith measure for raw measurements without judgment.
  • Versioned JSON output with schema_version.
  • A self-contained HTML report command.
  • Stable kebab-case check ids such as time-monotonic, nan, quat-norm, quat-flip, duration-sanity, scale-keys, non-uniform-scale, constant-nonunit-scale, constant-track, loop-seam, gait-group, root-motion-speed, missing-bones, frozen-bone, in-place, foot-slide, fps, and bind-pose.
  • Safe mechanical repairs through fix, including quaternion normalization and hemisphere normalization.
  • Mechanical transforms such as slicing, hold extension, gait anchoring, and fps-aware transforms.
  • diff for comparing meaningful metric changes across asset revisions.

Genuinely new or underdeveloped areas surfaced by this research include:

  • Engine profiles distinct from rig profiles: Unity, Unreal, Godot, Bevy, and generic glTF/runtime behavior.
  • Engine import prediction and optional engine readback/smoke tests.
  • Bevy-specific asset addressability: GltfAssetLabel manifests, named animation inventories, target-id reports, and RON animation graph templates.
  • Event, curve, notify, contact, and gameplay-window metadata checks.
  • Duplicate loop-endpoint detection and safe removal.
  • Blend entry/exit pose checks against declared transition families.
  • Retarget bone-map, bone-length, and rest-pose risk diagnostics.
  • Runtime-facing sidecars, including contact events and engine import manifests.
  • Markdown report output, if a lightweight non-HTML artifact is useful.

The check-like names later in this document are conceptual categories, not proposed public ids. Existing shipped check ids are public contract: renaming or replacing them with dotted taxonomy names would require a separate pre-1.0 design decision.

The transform ideas also need to respect the current design guardrail: animsmith may rewrite clips only in ways whose correctness its own checks can verify. Retargeting, rest-pose rewriting, procedural foot locking, motion warping, stride normalization, and other artistic motion edits should be treated as ADR-level scope proposals unless they can be reduced to mechanical, measurable, reversible operations.

What "Game-Ready" Means

An animation clip is game-ready when it satisfies four contracts at once.

1. Runtime Contract

The engine can import, store, sample, blend, compress, retarget, and play the clip without surprises.

This includes:

  • Valid file structure and buffers.
  • Supported animation channels and interpolation modes.
  • Predictable sample rate or key distribution.
  • Finite values, normalized rotations, sane scale, and no hidden shears.
  • Reasonable key counts and memory footprint.
  • No unnecessary animated scale or zero-value curves unless intentionally used.

2. Character Controller Contract

The clip's spatial behavior matches the gameplay system that will drive it.

This includes:

  • Clear in-place vs root-motion classification.
  • Expected root bone, root node, or motion extraction source.
  • Horizontal, vertical, and rotational root motion treated consistently.
  • No foot sliding during planted phases beyond project tolerance.
  • No ground penetration or unintended floating.
  • Movement distance, stride, speed, and phase aligned with locomotion design.

3. Retargeting Contract

The clip can be used on the intended rigs without silent degradation.

This includes:

  • Compatible skeleton hierarchy, bone names, and bone count policy.
  • Known rest pose, bind pose, unit scale, and orientation.
  • Bone lengths and default rotations within tolerance.
  • Humanoid/quadruped/creature profile mapping where applicable.
  • No tracks that the target engine will discard without the user noticing.

4. Pipeline Contract

Artists, technical animators, engineers, and build systems can understand the asset state.

This includes:

  • Stable naming and clip slicing.
  • Actionable warnings, not vague pass/fail output.
  • JSON/HTML/Markdown reports for CI and reviews.
  • Profile-specific thresholds per engine/project.
  • A clear path from "failed" to "fix in DCC" or "auto-transform safely".

Engine Observations

Unity

Unity's Animation tab makes the production concerns explicit. The Manual describes Animation Clips as the smallest building blocks of Unity animation and lets imported FBX data become selectable clips. The importer includes asset-level controls such as import animation, baking IK/simulation to FK keyframes, resampling Euler curves to quaternions, and animation compression. It also exposes clip-level controls for frame ranges, Loop Time, Loop Pose, Cycle Offset, root transform rotation, root transform position Y, root transform position XZ, curves, events, and Avatar masks.

Root motion is treated as a first-class import decision. Unity computes a root transform projection from the body transform for humanoid clips, and lets the user decide whether rotation, vertical position, and horizontal position are baked into the pose or stored as root motion. Unity also warns that import messages can indicate that source data was discarded or that retargeted animation may not exactly match the source.

Implication for animsmith:

  • Checks should report whether a clip is an in-place candidate, root-motion candidate, or mixed/ambiguous.
  • Loop validation should not only compare first and last local poses. It should compare root-space pose continuity, root displacement continuity, and phase/contact continuity.
  • Reports should distinguish "engine will import" from "engine will drop or reinterpret data".
  • Compression readiness should be measurable before import: key counts, redundant tracks, high-frequency jitter, rotation/position/scale error estimates.

Primary references:

  • Unity Manual, Animation tab: https://docs.unity3d.com/Manual/class-AnimationClip.html
  • Unity Manual, How Root Motion works: https://docs.unity3d.com/Manual/RootMotion.html

Unreal Engine

Unreal's FBX animation pipeline says the FBX pipeline is the route for getting skeletal animations from DCC tools into Unreal, and notes that the UE FBX importer uses FBX 2020.2. The Animation Sequence docs define an Animation Sequence as keyframes for a Skeletal Mesh's Skeleton, with position, rotation, and scale sampled over time. Sequences are tied to Skeletons, which allows sharing across meshes using the same Skeleton.

Unreal's import settings expose the same pipeline concerns in engine terms: animation length, frame import range, sample rate, skeleton selection, importing custom attributes, bone tracks, redundant-key removal, and preservation of local transforms. Unreal also flags a practical issue: animations with non-whole end frame values do not import correctly. Root motion requires a skeleton with a root bone and root-bone motion data, then must be enabled per Animation Sequence or Montage. Unreal also separates root motion extraction modes and documents a performance cost when root motion requires animation graph work on the game thread.

Implication for animsmith:

  • An Unreal profile should check FBX version where detectable, whole-frame end ranges, root bone presence, root-bone motion, skeleton name/hierarchy compatibility, track scale usage, and custom attribute/event curves.
  • Root motion checks should include "root motion exists but is not safely extractable" and "root motion exists but looks like accidental drift".
  • Performance/readiness checks should classify clips by compression risk and redundant track count.

Primary references:

  • Unreal Engine, FBX Animation Pipeline: https://dev.epicgames.com/documentation/en-us/unreal-engine/fbx-animation-pipeline-in-unreal-engine
  • Unreal Engine, Animation Sequences: https://dev.epicgames.com/documentation/en-us/unreal-engine/animation-sequences-in-unreal-engine
  • Unreal Engine, Root Motion: https://dev.epicgames.com/documentation/en-us/unreal-engine/root-motion-in-unreal-engine

Godot

Godot's 3D scene import docs expose two relevant concepts for animsmith. First, the advanced import settings include animation optimization, saving animations to external files, and slicing one source timeline into multiple named animations with start/end frames and loop flags. Second, Godot's retargeting documentation emphasizes that bone names alone are insufficient: bone rest transforms and hierarchy matter, and models exported from different DCC tools can have different rest rotations.

Implication for animsmith:

  • A Godot profile should validate that a single source timeline can be sliced into named clips and that those slices have frame-accurate boundaries.
  • Retarget-readiness checks should include rest-pose/bone-rest diagnostics, not just bone-name mapping.
  • Reports should include "will share animation correctly" vs "imports but retargeting will likely distort".

Primary references:

  • Godot, Advanced Import Settings, Animation options: https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/importing_3d_scenes/advanced_import_settings.html
  • Godot, Retargeting 3D Skeletons: https://docs.godotengine.org/en/stable/tutorials/assets_pipeline/retargeting_3d_skeletons.html

Bevy

Bevy's animation path is different from Unity, Unreal, and Godot because the main engine-facing workflow is not an editor import inspector. In Bevy 0.19, the glTF plugin loads glTF 2.0 assets, exposes specific parts through GltfAssetLabel, and creates Bevy-side representations such as scenes, nodes, skins, animations, and named animations. The animation crate defines AnimationClip as curves mapped to AnimationTargetIds, AnimationPlayer as the playback controller, AnimationGraph as the blending surface, plus transitions, repeat modes, masks, and animation events.

The official examples show the practical contract: a skinned glTF scene can spawn an animation player, clips can be selected by numeric or named glTF animation labels, and an AnimationGraphHandle must be attached to drive playback. Bevy also supports serialized animation graphs in RON and runtime graph construction. That means Bevy-readiness is partly data quality and partly asset-addressability: the clip must be valid glTF, but it also needs stable names/indices, usable target IDs, predictable scene hierarchy, and graph/mask/event metadata that Rust systems can consume.

The glTF loader docs also matter for asset policy. Bevy documents supported and unsupported Khronos extensions; for animation-related planning, KHR_animation_pointer is not supported in the referenced Bevy 0.19 docs. The docs also warn that misspelled labels can be ignored when using string labels, recommending typed GltfAssetLabel for correctness. That makes name stability and manifest generation useful animsmith features for Bevy teams.

Implication for animsmith:

  • A Bevy profile should start from strict glTF validation, then add Bevy-specific checks for named animations, asset labels, scene/default scene availability, skins, animation target IDs, and graph-readiness.
  • Reports should include Bevy code-facing identifiers: scene labels, animation labels/indices, named animation keys, target IDs, skeleton root candidates, and whether an AnimationPlayer/graph setup can be inferred from the asset.
  • Animsmith should warn when clips depend on unsupported glTF extensions or data paths that Bevy will not load into ordinary AnimationClip curves.
  • For Bevy projects, sidecars are especially valuable: generated RON animation graph templates, Rust-friendly metadata JSON, animation event/contact sidecars, and import manifests can reduce fragile hardcoded indices.

Primary references:

  • Bevy docs.rs, bevy::animation: https://docs.rs/bevy/latest/bevy/animation/index.html
  • Bevy docs.rs, bevy::gltf: https://docs.rs/bevy/latest/bevy/gltf/index.html
  • Bevy example, Animated Mesh: https://bevy.org/examples/animation/animated-mesh/
  • Bevy example, Animation Graph: https://bevy.org/examples/animation/animation-graph/
  • Bevy example, Animated Mesh Control: https://bevy.org/examples/animation/animated-mesh-control/

glTF

glTF gives animsmith a precise low-level validation target. A glTF node hierarchy must be a set of disjoint strict trees, with no cycles and at most one parent per node. Animated nodes must use translation, rotation, and scale properties rather than matrix transforms. TRS is composed in T * R * S order. Animation interpolation modes define how values are evaluated between keyframes, with special handling for quaternion rotation interpolation.

Implication for animsmith:

  • glTF validation should include structural checks before semantic animation checks.
  • Animated matrix transforms should be reported as a format-level incompatibility.
  • Rotation tracks should be checked for normalized quaternions and interpolation-safe continuity.
  • Accessor bounds, finite values, monotonic timestamps, and channel target validity should be hard errors.

Primary reference:

  • Khronos glTF 2.0 Specification: https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html

What Should Be Verified

This section is written as a check catalog animsmith can gradually implement.

Format and Importability

Verify:

  • File parses cleanly.
  • All referenced buffers/images/accessors exist.
  • Node hierarchy has no cycles.
  • Animation channels point to valid nodes and supported target paths.
  • Animated transforms use supported TRS representation.
  • Timestamps are monotonic and finite.
  • Rotation values are valid quaternions.
  • No NaN, infinity, denormal-heavy data, or extreme coordinate values.
  • Declared duration matches actual key times.
  • File extensions, embedded/external buffers, and path references are portable.

Why it matters:

Bad file structure creates hard import failures. Slightly malformed animation data can create engine-specific behavior that is hard to diagnose after import.

Animsmith output:

  • Hard errors for broken references and unsupported animated transform representation.
  • Warnings for unusual but technically legal data, such as animated scale on many bones.
  • A "portable asset" score for pathing and external dependency stability.

Skeleton and Rig Compatibility

Verify:

  • Expected root bone or root node exists.
  • Skeleton hierarchy matches project profile.
  • Bone names match a configured naming map or known profile.
  • Bone parent-child relationships match target skeleton.
  • Rest pose and bind pose are available and consistent.
  • Bone lengths and default rotations are within tolerance.
  • No duplicate bone names where the engine/profile requires uniqueness.
  • No unnecessary mesh nodes inside bone hierarchy for profiles that treat those specially.
  • No unsupported helper/control bones unless explicitly ignored.

Why it matters:

Retargeting often fails as "almost correct". The clip imports, but shoulders twist, feet rotate, hips drift, or scale changes leak into the pose. Engine docs consistently make skeleton compatibility central to sharing animations.

Animsmith output:

  • Bone map report: matched, missing, duplicate, extra, parent mismatch, rest-pose mismatch.
  • Retarget risk level: low, medium, high.
  • DCC-oriented fix hints: rename, map, remove helper tracks, rebake rest pose, export skeleton only.

Clip Segmentation, Naming, and Timing

Verify:

  • Clip has stable name, action name, or slice definition.
  • Start/end frames are explicit and whole-frame when the target profile requires it.
  • Duration is above a useful minimum.
  • Start/end frame selection does not duplicate a pose in a way that causes a visible hitch.
  • Source timeline can be split into named clips without overlap mistakes.
  • Sample rate is expected or intentionally variable-keyed.
  • For looping clips, loop flag/policy is present or inferable.

Why it matters:

Many "bad clips" are bad cuts. A walk loop that includes both frame 0 and the repeated final frame may pause. An attack clip with a fractional end frame may import wrong in Unreal. A single mocap timeline without slices is not directly usable by designers.

Animsmith output:

  • Clip table: name, source take, start, end, duration, frame count, sample rate.
  • Slice validation: gaps, overlaps, fractional boundaries, duplicate endpoint risk.
  • Suggested split definitions for DCC/export/import tools.

Root Motion and In-Place Policy

Verify:

  • Root track exists when required.
  • Root node/bone is unambiguous.
  • Root translation and rotation are intentional, not tiny drift.
  • Horizontal XZ root motion matches gameplay expectations.
  • Vertical Y root motion is either baked into pose or extracted according to profile.
  • Root rotation is either baked into pose or extracted according to profile.
  • In-place clips have near-zero root displacement and near-zero accumulated drift.
  • Root-motion clips have coherent trajectory, speed, and final displacement.
  • Root path does not jump unexpectedly at clip boundaries.
  • Root lock policy can be expressed for target engine.

Why it matters:

Root motion is where animation and gameplay meet. Unity, Unreal, and Godot all expose controls that decide whether motion moves the character object, stays in the pose, or is discarded. The same raw data can be correct for one gameplay model and wrong for another.

Animsmith output:

  • Classification: in-place, root-motion, vertical-motion, rotational-root, ambiguous, broken.
  • Metrics: displacement, facing delta, average speed, peak speed, vertical travel, path curvature.
  • Policy diff: what changes if the clip is imported as Unity in-place, Unity root-motion, Unreal root-motion, or generic glTF playback.

Loop Quality

Verify:

  • First and last poses match within tolerance after accounting for root-space projection.
  • Root displacement per cycle is expected.
  • Velocity at loop boundary is continuous.
  • Foot/contact phase is continuous.
  • Upper-body additive layers do not pop at wrap.
  • Quaternion continuity does not flip at wrap.
  • Loop contains a complete gait cycle where expected.
  • Loop has no hidden endpoint hold.

Why it matters:

Engine loop toggles do not make a bad loop good. They repeat the data. A good loop must preserve pose, velocity, root-space relation, and contacts.

Animsmith output:

  • Loop score with component deltas: root position, root rotation, skeleton pose, key contact bones, velocity.
  • Suggested trim offsets, such as "remove duplicated final frame" or "start at opposite foot contact".
  • Optional transform: distribute small loop-pose correction across the clip, similar in spirit to an import-time loop-pose adjustment.

Contact, Grounding, and Foot Slide

Verify:

  • Feet, toes, hands, weapon tips, or configured contact bones respect contact windows.
  • Planted foot horizontal velocity remains below threshold.
  • Planted foot vertical position is near floor or contact plane.
  • Feet do not penetrate the ground beyond tolerance.
  • Feet do not float during expected support phase.
  • Contact events align with visual contact frames.
  • Stride length and cadence are plausible for the character scale.
  • Character root speed matches observed foot motion.

Why it matters:

Foot slide is one of the fastest ways for a game animation to feel ungrounded. It also exposes root-motion/in-place mismatch, scale mismatch, bad retargeting, and bad trimming.

Animsmith output:

  • Foot-slide report per foot/toe/contact bone.
  • Contact timeline and inferred support phases.
  • Warnings for "root speed inconsistent with stride" and "floor plane appears offset".
  • Optional event generation: footstep left/right markers and contact windows.

Blend and State-Machine Friendliness

Verify:

  • Entry/exit poses are compatible with expected transitions.
  • Idle/start/stop/turn clips have appropriate neutral or authored transition frames.
  • Locomotion clips expose phase for blend trees or motion matching.
  • Directional clips use consistent forward axis and speed labels.
  • Additive clips have a correct reference pose/frame.
  • Attack/ability clips expose anticipation, active, recovery, and cancel windows if configured.
  • Facial/upper-body overlays do not animate lower-body tracks unless intended.

Why it matters:

A single clip can look good in isolation and still be awkward in a state machine. Designers and animation engineers need phase, event, and transition metadata, not just poses.

Animsmith output:

  • Phase metrics for loops.
  • Entry/exit pose deltas against configured transition families.
  • Metadata coverage report: events, curves, notifies, gameplay windows.
  • Suggested tags: locomotion, idle, start, stop, turn, additive, attack, hit-react.

Runtime Performance and Compression

Verify:

  • Total key count and key density.
  • Per-track key count.
  • Redundant keys.
  • Tracks with constant values.
  • Animated scale track count.
  • Curves with all-zero values.
  • High-frequency jitter that compression will preserve or amplify.
  • Estimated error under key reduction.
  • Memory footprint estimate before and after cleanup.

Why it matters:

Unity and Unreal both expose key reduction/compression as part of normal animation import. Compression is not just an engine-side detail; bad raw data makes compression less effective and more visually risky.

Animsmith output:

  • Key-count budget pass/fail.
  • Redundant track report.
  • Compression-risk score.
  • Suggested safe transforms: drop constant scale tracks, remove zero curves, reduce keys under configured tolerance, resample only where needed.

Metadata, Events, Curves, and Gameplay Signals

Verify:

  • Clip has required events for its type.
  • Gameplay windows are present and ordered.
  • Curves use expected names and value ranges.
  • Footstep/contact events align with inferred contact frames.
  • Root motion/in-place tags match measured behavior.
  • Custom attributes are supported by target profile.
  • Events are not outside clip range after trimming.

Why it matters:

Gameplay often depends on animation metadata: footsteps, hit frames, invulnerability windows, VFX triggers, audio, IK weights, look-at masks, turn curves, stride warping, and blend weights. Raw assets usually do not arrive with this information in a project-specific form.

Animsmith output:

  • Metadata completeness report.
  • Event alignment warnings.
  • Exportable event/curve sidecars for engines that cannot preserve the source representation.

Asset Addressability and Runtime Wiring

Verify:

  • Each clip can be addressed by stable name, index, label, or manifest entry.
  • Named animations and numeric animation indices match the target runtime's expected paths.
  • Scene/default scene labels are present for runtimes that spawn animated scenes from asset labels.
  • Animation target identifiers can be mapped back to bones/entities after import.
  • Animation graph, blend tree, mask, or state-machine inputs can refer to the intended clips without hardcoded guesswork.
  • Event/contact sidecars use stable clip IDs and frame/time coordinates after trimming or resampling.
  • Required engine/runtime features are supported by the asset loader, such as glTF extensions and animation channel types.

Why it matters:

This is especially important for Bevy and custom runtimes. A clip can be valid glTF and still be awkward to use if the project has to rely on fragile numeric indices, unnamed clips, missing default scenes, unsupported extensions, or target IDs that are hard to connect to gameplay code.

Animsmith output:

  • Bevy/glTF asset manifest: scenes, animations, named animations, skins, target IDs, root candidates, and labels.
  • Runtime wiring warnings: missing default scene, unnamed animation, unsupported extension, ambiguous animation target, graph/mask metadata missing.
  • Optional generated templates: Bevy GltfAssetLabel manifest, RON animation graph skeleton, and event/contact sidecar.

Common Asset Process: Raw to Game-Ready

A practical game animation pipeline often looks like this:

  1. Acquire source asset

    Sources include mocap capture, marketplace packs, outsourced DCC files, Mixamo-style libraries, procedural generation, animation retarget output, or internal Maya/Blender exports.

  2. Normalize and preserve source

    Keep raw source immutable. Record source author, license, DCC package, export settings, units, skeleton profile, and intended target engine.

  3. Parse and inspect

    Validate file structure, skeleton, clips, track counts, root motion, sample rates, durations, and metadata. This is where animsmith should operate first.

  4. Segment clips

    Split source takes or long timelines into named gameplay clips. Trim endpoints. Decide loop flags. Remove accidental duplicate final frames. Record start/end frames in versioned config.

  5. Decide root motion policy

    For each clip, choose in-place, root-motion XZ, vertical root motion, rotational root motion, or baked pose. This should be explicit in config and checked by animsmith.

  6. Retarget and conform

    Map to project skeleton, apply rest-pose corrections, remove helper/control tracks, normalize scale, and rebake transforms where needed.

  7. Validate motion semantics

    Check loop continuity, foot contacts, root trajectory, stride, phase, floor contact, event windows, and blend entry/exit.

  8. Optimize

    Remove redundant keys/tracks, compress or prepare for engine compression, remove unsupported curves, and resample only when it improves portability.

  9. Export engine-facing artifacts

    Export FBX, glTF/GLB, engine-specific sidecars, event JSON, import presets, or generated clips.

  10. Engine import smoke test

Import into Unity/Unreal/Godot where possible, capture warnings, compare engine-readback metadata, and verify the clip still matches measured expectations.

  1. CI gate and report

Run checks in continuous integration. Attach HTML/Markdown reports to asset review. Track metric diffs across revisions.

How Animsmith Can Help

Animsmith should position itself as a buildable, scriptable counterpart to engine import inspectors. Engines already show many of the problems, but they show them late, inside editor workflows, and often after engine-specific conversion has already happened. Animsmith can catch and explain those problems before import.

Core Value Proposition

Animsmith helps teams answer:

  • Will this clip import into my target engine?
  • Will the engine reinterpret, discard, or compress data in a way that changes motion?
  • Is this clip in-place or root-motion, and does that match the gameplay contract?
  • Is this loop actually seamless?
  • Are the feet grounded and contacts usable?
  • Can this clip retarget to our skeleton?
  • Are the events, curves, and gameplay windows present?
  • Did this asset revision make the clip better or worse?

Stakeholder Value

For gameplay engineers:

  • Fewer movement/controller mismatches.
  • Measurable root motion, speed, stride, and phase.
  • CI gates for animation regressions.
  • JSON output that can feed gameplay tooling.

For animation engineers and technical animators:

  • Clear rig, retarget, root, loop, contact, and compression diagnostics.
  • Batch validation across libraries.
  • Repeatable thresholds per character, engine, and clip family.
  • A safer way to prepare clips for blend trees, state machines, and motion matching.

For artists and animators:

  • Actionable reports instead of vague "import looked wrong" feedback.
  • Visualizable metrics: root path, foot contact, loop error, pose deltas.
  • Fix hints that map to DCC actions: trim frames, freeze scale, bake keys, rename bones, remove helper controls, fix rest pose.

For producers and asset pipeline owners:

  • Reduced review churn.
  • Better outsourced asset acceptance criteria.
  • A machine-readable definition of "done".
  • Library-wide inventory and quality metrics.

Product Requirements

Shipped: clip audit command

  • animsmith lint <asset> already runs structural, skeleton, clip, root-motion, loop, contact, and performance checks.
  • It supports project config, stable exit codes, text output, and JSON output.
  • Future work should extend this surface with engine profiles rather than introduce a parallel check command.

P0/P1: Engine profiles

  • Built-in profiles: generic, unity-generic, unity-humanoid, unreal, godot, bevy, gltf-runtime.
  • Profiles define root policy, unit/axis expectations, skeleton profile, clip boundary rules, loop thresholds, contact bones, and unsupported track behavior.

Shipped: metrics command

  • animsmith measure already reports raw measurements without judgment.
  • Future work should add missing engine- and metadata-oriented metrics: asset labels, event windows, transition pose deltas, duplicate endpoint hints, import-risk fields, and Bevy target-id summaries.

Shipped/P1: report formats

  • The HTML report command already provides a human-readable artifact.
  • JSON output is already versioned and should remain the machine-readable source of truth.
  • Markdown output would be new and useful for CI comments, issue filing, and lightweight design reviews.
  • Any report should keep exposing:
    • "What failed"
    • "Why it matters in engines"
    • "How to fix"
    • "Which profile threshold was used"
    • "Raw metric values"

Shipped/P1: Configuration

  • animsmith.toml already provides project config for rig profiles, checks, clips, thresholds, and expectations.
  • Future config work should add engine-profile settings for:
    • skeleton profiles
    • root bones/nodes
    • contact bones
    • floor plane
    • clip families
    • thresholds
    • required metadata/events
    • engine profile

P1: Transform command

  • Safe transforms:
    • split/slice clips
    • trim frames
    • remove duplicate endpoint frame
    • normalize quaternion signs
    • drop redundant constant tracks when the result is provably equivalent
    • remove all-zero curves when the target profile permits it
    • generate footstep/contact events

Transforms that alter authored motion need a separate design decision: root-motion/in-place conversion, retargeting, rest-pose rewriting, floor alignment, key reduction with error tolerance, motion warping, procedural foot locking, and stride normalization are valuable ideas but exceed the current "mechanical and check-verifiable" bar unless narrowed.

P1: Diff command

  • Compare raw vs transformed clip and asset revision vs previous revision.
  • Report motion deltas, loop score changes, key count changes, foot-slide changes, and retarget risk changes.

P1: Engine-import preset generation

  • Generate suggested Unity/Unreal/Godot import settings, Bevy asset manifests, or sidecar instructions:
    • loop flags
    • root motion node/bone
    • sample rate
    • clip ranges
    • compression tolerances
    • event/curve metadata
    • Bevy GltfAssetLabel paths and optional animation graph templates

P2: Visual report

  • Web/HTML viewer with:
    • root trajectory
    • contact timeline
    • per-bone track summary
    • loop boundary pose delta
    • foot slide heatmap
    • before/after transform comparison

P2: Engine smoke tests

  • Optional integration to import assets into Unity, Unreal, Godot, or a Bevy runtime harness and capture import warnings.
  • Compare engine-side imported clip metadata to animsmith's predictions.

P3: DCC integrations

  • Blender/Maya scripts or plugins that open animsmith reports at the failing frame/bone.
  • Export profile templates.
  • One-click "validate before export".

Check Requirements

The following are capability categories for planning. They should map onto existing stable ids where those exist, not replace them.

Shipped or partially shipped categories:

  • format.parse
  • format.references
  • animation.time_monotonic
  • animation.finite_values
  • animation.quaternion_validity
  • animation.channel_target_validity
  • skeleton.root_exists
  • skeleton.hierarchy_profile
  • skeleton.rest_pose_presence
  • clip.duration
  • clip.boundaries
  • clip.whole_frame_end for Unreal profile
  • root.classification
  • root.drift_in_place
  • root.motion_presence
  • loop.pose_delta
  • loop.root_delta
  • loop.velocity_delta
  • contact.foot_slide
  • contact.floor_penetration
  • performance.key_count
  • performance.constant_tracks
  • performance.animated_scale

New or underdeveloped categories:

  • retarget.bone_map_quality
  • retarget.rest_pose_delta
  • retarget.bone_length_delta
  • compression.estimated_error
  • compression.jitter_risk
  • blend.entry_exit_pose_delta
  • phase.gait_cycle_consistency
  • metadata.event_alignment
  • engine.unity_import_prediction
  • engine.unreal_import_prediction
  • engine.godot_import_prediction
  • engine.bevy_import_prediction
  • bevy.gltf_asset_labels
  • bevy.animation_target_ids
  • bevy.animation_graph_readiness
  • metadata.required_events

Transform Requirements

Safe transforms should be explicit, reversible where practical, and reported as patches to the animation data.

Initial transforms:

  • Trim clip to frame/time range.
  • Split source timeline into named clips.
  • Remove duplicated loop endpoint.
  • Normalize quaternion signs to avoid interpolation flips.
  • Drop constant tracks below tolerance.
  • Drop all-zero curves not required by profile.
  • Resample to target sample rate.
  • Bake or extract root XZ/Y/rotation according to profile.
  • Floor-align root or contact plane where clearly configured.
  • Generate contact events from foot/toe analysis.

High-risk transforms should require explicit opt-in:

  • Retargeting.
  • Rest-pose rewriting.
  • Loop-pose correction distribution.
  • Key reduction with nonzero error tolerance.
  • Procedural foot locking.
  • Motion warping or stride normalization.

Documentation Animsmith Should Cover

Documentation Landing Page

Explain animsmith in one sentence:

Animsmith validates, measures, reports, and prepares skeletal animation clips for game-engine import and runtime use.

Then show three concrete examples:

  • "Check whether a walk cycle is a good in-place loop for Unity."
  • "Verify whether an FBX attack animation has a valid Unreal root bone, whole-frame end, and gameplay events."
  • "Generate a Bevy-friendly glTF animation manifest with stable labels, contact events, and graph-ready clip metadata."

Concepts Guide: What Is Game-Ready?

Cover:

  • Game-ready vs visually good.
  • Clip, take, sequence, action, slice.
  • In-place vs root motion.
  • Root bone vs root node vs body/root transform.
  • Loop quality.
  • Contact and foot slide.
  • Retargeting readiness.
  • Compression readiness.
  • Events, curves, notifies, and gameplay windows.

Engine Profile Guides

Create separate pages:

  • Unity profile
  • Unreal profile
  • Godot profile
  • Bevy profile
  • glTF profile
  • Generic runtime profile

Each page should include:

  • What the engine expects.
  • What animsmith checks.
  • Common failures.
  • How to configure thresholds.
  • How to fix failures in DCC or engine import settings.

Artist-Facing Failure Catalog

For every warning/error, document:

  • Symptom in engine.
  • What animsmith measured.
  • Common cause.
  • Fix in Blender/Maya/MotionBuilder where possible.
  • Fix by animsmith transform where safe.
  • When to ask an animation engineer.

Examples:

  • Foot slides during planted phase.
  • Loop pops at wrap.
  • Root drifts in an in-place idle.
  • Attack moves mesh away from capsule.
  • Clip imports but retargeted shoulders twist.
  • Scale tracks are discarded or cause compression issues.
  • Animation has fractional end frame.
  • Source timeline contains multiple unnamed clips.

Engineer-Facing Reference

Cover:

  • CLI commands.
  • Config schema.
  • Engine profile schema.
  • JSON report schema.
  • Exit codes.
  • CI examples.
  • Batch mode.
  • Diff mode.
  • Embedding/library API.

Pipeline Guide

Show recommended workflows:

  • Marketplace pack intake.
  • Mocap cleanup gate.
  • Outsourced asset acceptance.
  • Character skeleton migration.
  • Pre-commit asset checks.
  • CI checks on animation asset changes.
  • Raw vs transformed artifact storage.

Roadmap Recommendations

First Milestone: Trustworthy Audit

Goal: animsmith becomes useful before it mutates files.

Deliver:

  • Engine profiles.
  • Check catalog.
  • JSON and Markdown/HTML reports.
  • Metrics inventory.
  • Root/in-place classification.
  • Loop and foot-slide checks.
  • Configurable skeleton/contact profiles.
  • CI-ready exit codes.

Second Milestone: Safe Preparation

Goal: animsmith can fix common mechanical issues whose correctness it can verify.

Deliver:

  • Clip splitting/trimming.
  • Duplicate endpoint removal.
  • Provably equivalent track pruning.
  • Quaternion normalization.
  • Fps-aware resampling with measured before/after diff.
  • Contact event generation.
  • Diff report for every transform.

Third Milestone: Engine Feedback Loop

Goal: animsmith predicts and verifies real engine import behavior.

Deliver:

  • Unity/Unreal/Godot import setting generation.
  • Bevy asset manifest and animation graph template generation.
  • Optional headless/editor import smoke tests.
  • Optional Bevy runtime smoke-test harness.
  • Import warning capture.
  • Engine-readback comparison.
  • Profile-specific docs generated from check metadata.

Fourth Milestone: Retargeting Diagnostics and Authoring Assistance

Goal: animsmith helps technical animators diagnose retargeting and library quality problems without silently becoming a DCC retargeter.

Deliver:

  • Bone map validation.
  • Rest-pose diagnostics.
  • Retarget risk scoring.
  • Motion library inventory.
  • Batch reports and dashboards.
  • DCC helper plugins.

Actual retargeting, rest-pose rewriting, and authored-motion repair would need an explicit design-record update before becoming product scope.

Product Positioning

Animsmith should not be positioned as "another converter" or "another animation editor". The stronger position is:

Animsmith is a content quality gate for animation clips. It gives animation teams the same repeatable, reviewable, CI-friendly confidence that code teams expect from tests and linters.

The pitch differs by audience:

  • Artists: "See exactly why the clip fails and which frame/bone needs attention."
  • Technical animators: "Codify engine import rules and retargeting assumptions once, then batch-apply them."
  • Gameplay engineers: "Stop debugging capsule/root/foot-slide problems after import."
  • Producers: "Turn outsourced animation acceptance into measurable requirements."
  • Tool engineers: "Use a library and JSON schema rather than scraping engine import logs."

Suggested Issue Backlog

New issues worth filing after dedupe:

  • Add duplicate endpoint detection and removal.
  • Add engine profile config model distinct from rig profiles.
  • Add engine import prediction for Unity/Unreal/Godot/Bevy profiles.
  • Add Unity/Unreal/Godot/Bevy profile docs.
  • Add Bevy glTF asset-label and named-animation manifest generation.
  • Add Bevy animation graph template generation.
  • Add generated contact events sidecar.
  • Add gameplay event/window metadata checks.
  • Add blend entry/exit pose-delta checks against transition families.
  • Add retarget bone-map and bone-length diagnostics.
  • Add Markdown report output.
  • Add engine import preset generation.
  • Add headless import smoke test adapters.
  • Add Bevy runtime smoke-test harness.
  • Add compression error estimator.
  • Add batch library dashboard.

Ideas that need ADR or design-record updates before issue filing:

  • Root-motion to in-place conversion, or in-place to authored trajectory.
  • Retargeting and rest-pose rewriting.
  • Procedural foot locking, motion warping, or stride normalization.
  • Key reduction with nonzero visual/motion error tolerance.

Appendix: Source Notes

  • Unity's Animation tab documents import-level controls for animation clips, compression, clip ranges, looping, root transform controls, curves, events, masks, root motion node source, import messages, and retargeting warnings.
  • Unity's Root Motion guide documents body/root transform behavior, baking rotation/Y/XZ into pose, root transform projection, gravityWeight implications, and loop pose behavior.
  • Unreal's FBX Animation Pipeline documents the FBX import path, FBX 2020.2 requirement, animation export/import expectations, skeleton-only animation export, and skeleton selection.
  • Unreal's Animation Sequences guide documents skeletal keyframe data, skeleton sharing, import options, whole-frame end frame issue, editing, sharing/retargeting, and compression.
  • Unreal's Root Motion guide documents root bone requirements, root motion extraction, root lock options, debug visualization, and runtime/performance implications.
  • Godot's Advanced Import Settings document animation optimizer, saving animations to file, and timeline slices.
  • Godot's Retargeting 3D Skeletons guide documents bone rest, bone map, skeleton profile, and rest-pose alignment concerns.
  • Bevy's animation docs describe AnimationClip, AnimationPlayer, AnimationTargetId, AnimationGraph, transitions, repeat modes, masks, and events as the runtime animation surface.
  • Bevy's glTF docs describe loading glTF 2.0 assets, using typed GltfAssetLabels for scenes and animations, named asset access, coordinate conversion utilities, skins, and supported/unsupported Khronos extensions.
  • Bevy's animation examples show skinned glTF playback, named animation selection, animation graph blending, serialized graph assets, and attaching an animation graph handle for runtime playback.
  • Khronos glTF 2.0 documents hierarchy, TRS animation constraints, animation interpolation behavior, skins, accessors, and binary data rules.

All pages

Every page AnimSmith publishes, one row each — each page owns one job. The site's front door, which routes the two reader journeys and the ten-minute path, is at https://mmannerm.github.io/animsmith/.

The Category column names the documentation site's part and optional Part › Group, so this table and the site sidebar carry the same structure in the same order.

DocumentUse it to…Category
Why animsmithDecide whether animsmith fits your team — what it is, why it exists, and what it is worth by role. The canonical home of the positioning case.Start
What game-ready meansRead the staged readiness ladder — what "game-ready" is evidence for at each level, who owns that level, and what one lint run does and does not settle.Start
InstallGet the binary — prebuilt archive, cargo install, or the pure-Rust glTF-only build — and confirm it runs.Start
First lint in 60 secondsSee a finding, repair what is mechanically safe, and watch a declared contract catch a popped loop, on two committed sample clips.Start
Your first reportTurn findings into skeleton playback, charts and one shareable HTML file, including the before/after comparison and the evidence-only form.Start
Mixamo tutorialTake a real Mixamo download end-to-end — download, convert, inspect, lint, fix, and grow a contract config with the built-in mixamo rig profile.Start
Symptom indexStart from what you see in the engine and route it to the page, check, repair and config that address it.Symptoms
The pose flickers, spins, or explodesRepair the rotation representation itself: non-unit keys, hemisphere flips, non-finite values, and what fix restores losslessly.Symptoms
The clip is the wrong length or freezes at the endFind the channel that stopped early or the export range that drifted, and pin the duration and frame grid the clip owes you.Symptoms
The loop popsFix a looping clip that jumps or hitches at the wrap: what the seam checks measure, the before/after evidence, and the loop contract.Symptoms
The character glides or runs in placeSettle whether gameplay or the animation owns horizontal movement, and hold the clip's measured travel to that decision.Symptoms
Feet skate when clips blendHold a directional set to one stride phase and one timing surface, and know which member is out.Symptoms
Feet slide within a clipFix a planted foot that skates during stance: the sampled stance intervals, the declared speed, and who owns the repair.Symptoms
A limb is T-posed, or a bone never movesSeparate a bone that is absent from one that is keyed but frozen, and both from a clip authored against another bind.Symptoms
Files disagree about skeleton or clip identityKeep the exact (file, clip) identity of a pack instead of trusting a repeated display name, and decide what a retarget still owes you.Symptoms
The file is bloated, or the retargeter chokesDecide what is redundant exported data and what is authored scale, then remove only the part a transform can prove.Symptoms
For artists: from export to handoffTake an authored export from immutable source through evidence-backed candidate handoff, without treating a safe mechanical edit as artistic approval.Workflows
For game developers: from pack to engine gateTake a pack or collection from inventory through an exact engine profile and an engine-observed gate; includes the complete Bevy 0.19.0 path.Workflows
Declaring the contract: who writes whatSettle who owns each animsmith.toml surface — which parts an animator declares alone, which are the game's call, and where each value comes from — with one complete minimal config.Workflows
Pipeline scenario guidePlan a raw-to-game-ready asset process — marketplace intake, mocap cleanup, outsourced acceptance, CI gating, and artifact storage.Workflows
Unity 6000.3 profile guideConfigure the exact Unity Generic/Humanoid profiles, importer advice, root-motion choices, and scale boundary.Workflows › Engine profiles
Unreal Engine 5.8 profile guideMap FBX animation, Skeleton, frame, unit, root-motion, and scale concerns to AnimSmith evidence without inventing importer settings.Workflows › Engine profiles
Godot 4.7 profile guidePlan scene import, retargeting, animation slicing, root scale, and the current profile's explicit prediction boundary.Workflows › Engine profiles
Bevy 0.19.0 profile guideGenerate exact Animation{i} selector evidence and keep runtime loading, graph, target, and scale responsibilities explicit.Workflows › Engine profiles
glTF and generic runtime guideUse the engine-neutral contract for custom runtimes, glTF units, source identity, scale repair, and downstream validation.Workflows › Engine profiles
Examples cookbookDo the work, copy-paste style — gate exports in CI, repair a broken export, trim or re-anchor a clip, encode a project contract config, migrate FBX/Mixamo exports, embed the checks in Rust.Workflows
Static asset workflow guideDiagnose bounds and transform domains, preserve normal maps, bake supported static placement, attach explicit textures, and understand what still needs engine validation.Workflows › Advanced workflows
Scaling glTF safelyChoose whole-document unit conversion or rest/bind reparameterization, understand the exact-source rewrite/proof transaction, and interpret its support boundary.Workflows › Advanced workflows
Material texture recipesAttach explicit BaseColor, normal, metallic-roughness, and occlusion images during conversion with deterministic resizing and provenance evidence.Workflows › Advanced workflows
Multi-source character assemblyCombine an authoritative skinned base with exact takes and timeline windows from separate inputs, producing one deterministic GLB plus evidence.Workflows › Advanced workflows
Collection contract extensionsRead the contact-fragment, foot-cycle planner, and transition-family contracts, including strict manifest and contact-evidence bindings.Workflows › Advanced workflows
Configuration referenceComplete animsmith.toml tables, keys, defaults, precedence, validation, globs, rig profiles, and engine settings.More › Reference
Built-in check referenceLook up every registered built-in check: current IDs, default findings, prerequisites, config keys, gap semantics, and remediation boundaries.More › Reference
CLI referenceLook up a command, flag, or exit code.More › Reference
Machine-readable outputParse versioned --format json reports, glTF animation-addressability inventories, engine import advice, and producer evidence in a pipeline, validated by the JSON Schema under schemas/.More › Reference
Commercial-pack evaluation guideRead the maintained Mixamo and Protofactor technical-report/evidence pairs and separate their scoped findings from project acceptance.More › Pack evaluations
Animation-pack evaluation reportsReview evidence-backed marketplace-pack assessments and the reporting convention for constituent packs and collection-level cross-pack summaries.More › Pack evaluations
Embedding guideChoose library crates and integration boundaries, then follow the embedded gate flow with the runnable embed example.More › Rust integration
docs.rs API referencesLook up exact published Rust API contracts for animsmith-core, animsmith-gltf, animsmith-fbx, animsmith-engine, and animsmith-report.More › Rust integration
CONTRIBUTING.mdWork on animsmith itself — contribution flow.More › Project and contributing
DEVELOPMENT.mdSet up a checkout and run the local verification commands.More › Project and contributing
RELEASING.mdCut a release — the release-plz flow, the manual 0.1.0 bootstrap, and the published-doc-link policy.More › Project and contributing
SUPPORT.mdGet help or file a bug.More › Project and contributing
SECURITY.mdReport a vulnerability privately.More › Project and contributing
DESIGN.mdFollow the architecture, check-catalog rationale, and roadmap.More › Project and contributing
Scale proof calibrationReview the implementation-owned calibration sweep, provenance magnitudes, historical policy measurements, and reproducible command behind appendix-d-v6.More › Project and contributing
research/Read the dated research notes that inform the roadmap.More › Project and contributing
All pagesFind any page by the job it owns — this table, which is also where the site's navigation comes from.More