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

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