Skip to content

Contributing

This page is the operational half of the project: the gates a change has to pass, how to run each one, and what to do when one of them reddens. It assumes you have a clone; Install covers getting one and what toolchain it needs.

Everything here is runnable locally. There is no gate that only exists in CI.

The house rules, in six lines

  1. No new unreviewed unsafe. Workspace crates forbid it except for the reviewed operations in atune_py (inventory). Any widening needs a soundness argument and focused tests.
  2. Do not panic to report a library failure. Return Result and use thiserror; unwrap/expect only in tests or provably-infallible spots that carry a comment saying why. User-provided Rust callbacks may unwind through library calls; do not catch them unless a specific boundary requires it.
  3. atune_core stays browser-clean. No clock, no filesystem, no threads in what compiles for wasm32; everything that needs an operating system sits behind the system feature.
  4. Determinism is a feature. Every source of randomness is seeded and derived, every iteration order is stable.
  5. Public items are documented, and doc examples compile.
  6. No gate is weakened to make a change pass. Not with an #[allow], not with an #[ignore], not with a wildcard that hides a difference. A gate that cannot go red is a gate nobody reads.

Dependency changes

Every direct dependency needs either a compiled source use or a written reason in its manifest covering a build script, procedural macro, generated surface, target-specific path, example, test, or optional feature. Before adding or removing an edge, inspect the package in its native, WASM, no-default, and relevant solo-feature configurations; workspace feature unification is not proof that the edge is necessary or unnecessary.

Pinned cargo-machete 0.9.2 is the supported inventory and a protected Rust gate. A reported edge is a review prompt, not automatic deletion: verify the cases above, make the smallest manifest change, update Cargo.lock, then rerun the affected isolated checks and cargo machete. Put a narrowly explained package.metadata.cargo-machete.ignored entry beside a proven false positive; never silence a whole class. Do not make cargo udeps a required gate until the project has an explicit false-positive policy for build scripts, proc macros, examples, tests, target dependencies, and optional features. Transitive version duplicates are upstream graph facts unless a first-party edge can remove them without changing behavior.

CI actions use reviewed full commit SHAs; tool and interpreter inputs use exact versions. Automated update proposals still require review of the upstream diff and the affected workflow before the pin moves. Python documentation inputs are declared in docs/requirements.in; the universal, hash-checked docs/requirements.txt and its freshness check own the resolved graph.

The gates

Run all of these before proposing a change. They are ordinary commands, in the order that fails fastest.

# What it checks Command
1 Formatting cargo fmt --all --check
2 Lints, warnings as errors cargo clippy --workspace --all-targets --all-features -- -D warnings
3 Tests, excluding doctests cargo test --workspace --all-targets
4 Doctests cargo test --workspace --doc
5 The core without its default features cargo test -p atune_core --all-targets --no-default-features
6 The storage conformance guide's own examples cargo test -p atune_core --features conformance --doc
7 The browser target cargo check -p atune_core --target wasm32-unknown-unknown --no-default-features, then the same with --all-features, then cargo check -p atune --target wasm32-unknown-unknown --no-default-features --features remote
8 Rustdoc, both feature sets RUSTDOCFLAGS='-D warnings' cargo doc --workspace --no-deps, then the same with --all-features
9 Generated reference pages are current cargo dev generate-all --mode check
10 Every Rust example runs cargo dev run-examples
11 Every Python example runs python -m pytest examples/python, after maturin develop --release -m crates/atune_py/Cargo.toml
12 Every CLI documentation block runs cargo test -p atune_cli --test cli_docs
13 Rust and Python agree, bit for bit cargo dev check-parity
14 The site builds with no warning uv run --with-requirements docs/requirements.txt mkdocs build --strict
15 The docs.rs build succeeds CARGO_TARGET_DIR=target/docsrs RUSTDOCFLAGS='--cfg docsrs' cargo +nightly doc -p <crate> --all-features --no-deps, once per published crate (atune_core, atune_derive, atune, atune_cli) — one invocation each, not one over four: docs.rs builds crates alone, and a unified build is strictly weaker than the thing this gate predicts

The Rust workflow also runs these checks, which are not numbered local gates:

  • cargo test --locked -p atune_core --doc --no-default-features
  • cargo clippy --locked -p atune_core --all-targets --no-default-features -- -D warnings
  • cargo check --locked -p atune --all-targets --no-default-features
  • cargo check --locked -p atune_core --target x86_64-pc-windows-msvc and cargo check --locked -p atune --target x86_64-pc-windows-msvc, then the same two with --target aarch64-apple-darwin: default features, so the system platform cfg paths compile for Windows and macOS (neither crate enables sqlite by default)
  • cargo audit --deny warnings --ignore RUSTSEC-2024-0436, over every package in Cargo.lock (cargo deny sees only its configured targets), with the pinned cargo-audit in the workflow's CARGO_AUDIT
  • pip-audit --no-deps --disable-pip -r <lock> over every committed Python lock except the CPython 3.10 callback lock, from the hashed .github/release/pip-audit-requirements.txt

Python lock freshness is checked in CI by python3 docs/_build/check_requirements_lock.py (docs.yml) and python3 .github/release/check_python_locks.py (release-staging.yml).

Also run cargo check --workspace --all-targets --all-features as the workspace-wide all-feature compilation check (with the two GUI exclusions on 1.92.0, as below). PyO3's deprecated extension-module feature is not forwarded through Cargo: maturin sets PYO3_BUILD_EXTENSION_MODULE only for wheel builds, leaving Rust test binaries able to link libpython.

Four traps in that list, each of which has cost this project a bug:

  • --all-targets excludes doctests. Gates 3 and 4 are two commands because one of them cannot do both jobs.
  • --no-default-features must be per package. Workspace-wide, feature unification through another member turns the flag into a no-op.
  • A doctest inside a feature-gated module is invisible without the feature, which is why gate 6 exists as its own line.
  • All-on and all-off do not cover the single-feature combinations. A bug can live in exactly one feature set; CI runs the combinations individually — ten solo-feature test runs today — and so should you if you touched feature-gated code.

Two toolchain lines

The published crates (atune_core, atune_derive, atune, atune_cli) and the other members build on Rust 1.92, the MSRV. The two publish = false GUI crates, atune_gui and atune_showcase, declare rust-version = 1.95 because egui and eframe 0.37 need it (D125). On Rust 1.95 or newer the gates above run as written. On 1.92.0, add --exclude atune_gui --exclude atune_showcase to every workspace-wide command (gates 2, 3, 4 and 8, and the all-feature check), then run the GUI crates' gates on 1.95.0:

cargo +1.92.0 clippy --locked --workspace --exclude atune_gui --exclude atune_showcase --all-targets --all-features -- -D warnings
cargo +1.95.0 clippy --locked -p atune_gui -p atune_showcase --all-targets --all-features -- -D warnings
cargo +1.95.0 test --locked -p atune_gui -p atune_showcase --all-targets
cargo +1.95.0 check --locked -p atune_gui --target wasm32-unknown-unknown --no-default-features
cargo +1.95.0 check --locked -p atune_showcase --target wasm32-unknown-unknown --all-targets

CI does the same: the MSRV jobs exclude the two crates, the gui job in rust.yml (its GUI_RUST variable) runs their Clippy, tests, doctests, rustdoc and wasm builds, and docs.yml builds the wasm demo on that toolchain. Shared dependencies must still build on 1.92, so a lockfile change also needs cargo +1.92.0 check --locked --workspace --exclude atune_gui --exclude atune_showcase --all-targets --all-features and gate 7 on 1.92.0.

The Optuna interop gate uses an isolated CPython 3.10 environment installed with python -m pip install --require-hashes --only-binary=:all: -r .github/release/python-interop-requirements.txt. Point ATUNE_OPTUNA_PYTHON at that environment's interpreter, then run:

ATUNE_OPTUNA_PYTHON=/path/to/venv/bin/python cargo test --locked -p atune_cli --test optuna_export --test optuna_import --test optuna_rdb -- --ignored
ATUNE_OPTUNA_PYTHON=/path/to/venv/bin/python cargo test --locked -p atune --features optuna-compat,optuna-rdb --lib optuna_compat -- --ignored
ATUNE_OPTUNA_PYTHON=/path/to/venv/bin/python cargo test --locked -p atune --features importance --lib importance::tests::cross_check_against_live_optuna -- --ignored

These tests fail on a missing import or a version other than Optuna 5.0.0. The Rust workflow runs this gate on its self-hosted release runner, in an owned venv; the benchmark lock remains at its measured Optuna 4.9.0 baseline. Refresh-check Python locks with UV_PYTHON=python3.10 python3 .github/release/check_python_locks.py.

The installed-wheel adoption gate uses python-adoption-requirements.txt for Optuna 5.0.0, Plotly, the default importance evaluator and pytest; python-callback-requirements.txt pins Lightning, Stable-Baselines3, Gymnasium and CPU torch for Linux x86_64 / CPython 3.10 (glibc 2.28 or newer). Both locks live under .github/release/, include transitive hashes, and are refresh-checked by the same tool. Install with --require-hashes --only-binary=:all: and --extra-index-url https://download.pytorch.org/whl/cpu, then run python -m pytest crates/atune_py/tests/test_optuna_adapter.py -q and the three callback examples. Missing or wrong Optuna fails this gate; the optional dependency test uses an actual clean environment rather than skipping. Gate 11 needs these inputs for every example.

Numerical validation

For numerical work, pin each non-trivial algorithm with a multi-step exact golden computed independently in the test body, and inject equation errors to prove the test catches them. A quality claim needs a 64-or-more seed sweep with headroom below the measured rate; lead ignored demonstrations with a seed-independent structural claim. Put a compared optimum off every warmup or QMC point, and give each “beats baseline” oracle a differential arm and a regression bite. Test sign invariance by comparing Maximize(v) with Minimize(-v) for bit-identical decisions. Study harnesses must call Sampler::after_trial for complete, failed, and pruned trials. Pair benchmark tables by seed, compare only problems every arm ran, share ranks on ties, and exclude reparameterized duplicates. Preserve valid evaluation order and RNG draws; a changed seeded trajectory needs review, and regenerating a golden is not a repair.

When fixing a lint, check Clippy on stable, 1.98.0, and the crate's MSRV line: 1.92 for the published crates, 1.95 for atune_gui and atune_showcase. A stable suggestion may exceed the MSRV, and a lint expectation may stop firing on one toolchain; restructure the code when the fix is not portable. The 1.95 Clippy run also lints the workspace crates the GUI crates depend on.

Command environment

A local gate predicts CI only when it runs on CI's toolchain with CI's code generation. CI runs the gates on Rust 1.92.0 (the MSRV variable in .github/workflows/rust.yml), and the GUI crates' gates on Rust 1.95.0 (its GUI_RUST variable), with no compiler wrapper and no extra rustflags. The repository deliberately carries no rust-toolchain.toml (D60), so a local command inherits whatever the host's rustup default and ~/.cargo/config.toml say, and a newer default toolchain disagrees with CI on lints. Pin both per session before running the gates:

export RUSTUP_TOOLCHAIN=1.92.0     # or prefix each command: cargo +1.92.0 ...
                                   # (cargo +1.95.0 ... for the GUI crates)
export RUSTFLAGS=                  # set but empty: host-wide rustflags no longer apply
export CARGO_BUILD_RUSTC_WRAPPER=  # no sccache or other compiler wrapper
unset RUSTC_WRAPPER LD_PRELOAD
export CARGO_TARGET_DIR=/tmp/atune-$USER/target

An empty RUSTFLAGS replaces every build.rustflags and target.<triple>.rustflags entry in Cargo's configuration, which is what drops a host's -Ctarget-cpu=native: code built for the local CPU is not what CI or a wheel user runs. A link-only flag can stay, for example RUSTFLAGS=-Clink-arg=-fuse-ld=mold; code-generation flags (-Ctarget-cpu, -Ctarget-feature, -Copt-level) cannot. Newer toolchains are covered by the Clippy rule above: stable, 1.98.0, and the 1.92 or 1.95 MSRV line.

On a shared or hybrid-core host, also bound parallelism. available_parallelism() counts SMT siblings and efficiency cores, so the defaults oversubscribe a 32-thread desktop part with eight performance cores as soon as two sessions build at once:

export CARGO_BUILD_JOBS=8   # 8 to 16 on a 32-thread host; fewer under load
export RUST_TEST_THREADS=8

Three more command-use traps apply across local runs:

  • If an ambient LD_PRELOAD interferes with a command, run it with env -u LD_PRELOAD; the block above already does.
  • Give concurrent Cargo sweeps separate CARGO_TARGET_DIR values.
  • Read the test summary for filtered or ignored runs and confirm the expected cases actually ran; zero matching tests can still exit successfully.

Wall-clock bounds in tests flake on a loaded host. A timing failure that does not reproduce on an idle host is a test defect to report with its panic text, not a gate to rerun until it passes; prefer deterministic proofs (marker files, events, injected clocks) over elapsed-time assertions in new tests.

CI and hooks

The Rust workflow's release_gate and wasm jobs run the Rust checks on the MSRV, including the per-push extras cargo dev check-lints, cargo dev check-guide-links and the facade's no-default leg; its gui job runs the GUI crates' checks, their wasm32 builds included, on 1.95.0; its deny job runs cargo deny check advisories bans licenses sources (pinned cargo-deny). The documentation workflow owns Python examples, both arms of parity, cargo dev check-api-links (after a fresh cargo doc), the strict site build, and the per-crate docs.rs builds. Run the full gate list locally; CI jobs split those checks across their required build artifacts.

CI's Python interpreter is CPython 3.10.22, the final 3.10 release and the abi3-py310 floor, in every workflow and in the sanitizer build (D126). The macOS ARM64 and Windows wheel legs use 3.10.11, the last 3.10 release with a setup-python asset for those platforms. The documentation workflow's python-3-12 job reruns the installed-wheel regressions on CPython 3.12. It is optional: continue-on-error reports its failure without failing the run, and no other job waits on it.

One more layer exists beside the gates: pre-commit hooks (.pre-commit-config.yaml is the authority; pre-commit install turns them on). They are optional automation, not additional obligations — most mirror gates 1–4 and 8 — but a few checks live only there: the Miri target, codespell, and mdformat over the non-docs/ markdown. The Miri hook runs the 20-test atune_core snapshot integration target with --no-default-features and strict provenance on pinned nightly-2026-07-26; install it with rustup component add --toolchain nightly-2026-07-26 miri. It does not claim workspace, OS-backed Journal, faer, or PyO3 coverage; the ordinary backend suite, targeted sanitizer, and free-threaded-runtime evidence own those surfaces. Nothing in this table silently depends on the optional hooks. The Cargo hooks build, lint, and test the whole workspace on the active toolchain, so they need Rust 1.95 or newer, and a commit that touches Rust takes as long as gates 2–4 and 8 together, tens of minutes on a loaded host. pre-commit stashes unstaged changes while it runs: leave tracked files alone until it finishes, and after a failure check what it left staged before committing again. Format Markdown outside docs/ with the hook's own pinned mdformat (pre-commit run mdformat --files <file>): newer mdformat releases escape brackets differently, so a file a newer release accepts can still fail the hook. mdformat never reflows paragraphs; wrap lines by hand.

Hook revisions in .pre-commit-config.yaml are frozen to full upstream commit IDs, with the approved release label in a comment. To refresh one, resolve the approved tag with git ls-remote --tags <repository-url> refs/tags/<tag> 'refs/tags/<tag>^{}' (the ^{} line for an annotated tag), corroborate the commit upstream, change only that rev and comment, and run pre-commit validate-config plus the affected hooks. Never run pre-commit autoupdate; a version or behavior change needs its own review.

Gate 14 has a precondition: the Python API reference is produced by importing the module, so the build needs an activated virtual environment with the wheel in it. Without one it fails on that page rather than skipping it.

The installed-wheel Python regression gate covers active-run ownership and canonical constrained analysis. After building and installing the candidate wheel into the current environment, run from the repository root with that same interpreter:

PYTHONPATH= python -m pytest crates/atune_py/tests/test_installed_wheel.py crates/atune_py/tests/test_canonical_analysis.py -q

python -m pytest keeps collection and the tests' isolated child processes on the wheel interpreter, and a package-location assertion rejects a source-tree import.

Slow tests

The #[ignore]d GP sweep and heavy benchmark oracles stay out of the gates by cost alone; the slow CI job runs them on every non-PR Rust workflow event. Locally: cargo test -p atune_bench --release -- --ignored. A failure is a real regression. Live suites and release-only evidence are in Release evidence.

Gate 15 needs a nightly toolchain at or above the workspace's Rust version and runs without -D warnings on purpose: docs.rs publishes despite warnings, so the gate asserts only that the build succeeds. Gate 8 enforces rustdoc lints on stable.

The parity gate

Gate 13 runs the Rust and Python arms of every pair in examples/parity.toml. Each prints a canonical atune-parity/1 block (study, seed, trial count, sampler, and the winning trial's number, value, and parameters); the header and strings must be byte-equal, keys in the same ascending order, and floats equal as raw f64 bits. A failure names the pair, the first differing key, and both bit patterns. In order:

  1. One arm of a pair changed. Change the other to the same operation order, with no fused multiply-add on either side (f64::mul_add has no pre-3.13 Python counterpart).
  2. A sampler, scheduler, or seed derivation changed on purpose. Re-bless with ATUNE_BLESS_PARITY=1 cargo dev check-parity --rust-golden and read the golden diff; it is the record of the behavior change.
  3. Only best.value differs in the last bits. Run both arms on one machine: f64::cos and math.cos use the platform libm, and the goldens name the platform they were blessed on.
  4. The trial count differs. A history-dependent sampler or scheduler is running in parallel; parity examples using one run with parallelism one.
  5. None of the above. A real determinism bug; fix it.

Never add a tolerance or delete a pair to quiet the gate. Every Rust and Python example under examples/ appears in examples/parity.toml as a [[pair]] or an [[unpaired]] entry with a reason; an objective that cannot be spelled identically in both languages may use compare = "params-only" (parameters and trial number bit-equal, value within a declared tolerance), also with a reason.

Generated pages are not edited

See atune_dev's generator guide for output ownership and regeneration. Never hand-edit generated pages.

Documentation rules

  1. Guide pages stay short: about 200 hand-written lines, then split or delegate to a reference page.
  2. No hand-written program code: examples are macro calls over real files in examples/; short literals go in code spans, shell sessions may be fenced.
  3. A page is one file plus one nav: line in mkdocs.yml; either alone fails the build.
  4. An example is one file per language plus its parity.toml entry.
  5. docs/requirements.in is capped at four direct dependencies. Bounds on transitive packages go in docs/requirements-constraints.txt.

Build the site with uv run --with-requirements docs/requirements.txt mkdocs serve from an environment that has the wheel installed. The internal plan at the repository root is not part of the site: it is written for implementers, and no published doc comment or crate front page may cite it, because a reader of docs.rs or PyPI cannot open it.

A crate's README.md and its //! header are different documents on purpose. The README is the crate's shop window on crates.io; the //! header is the entry point to its API reference. The one part they share — the quickstart — is two copies kept in sync by hand.

Commits, and reporting things

Commit messages follow the repository's existing convention: a type and scope, then what changed — docs(guide): …, feat(core): …, fix(cli): ….

Report a bug as a GitHub issue with the smallest reproduction you can manage, including the version or commit, platform, and whether it reproduces with a single thread. Do not report a suspected vulnerability that way; the security policy has the private route.

Where to go next

If you want to Go to
Release-only evidence Release evidence
What a version number promises Versioning and stability
What changed Changelog
To write a sampler, scheduler or storage backend Extending
To publish your own as a crate Publishing a plugin crate
The determinism contract the parity gate asserts Determinism