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¶
- No new unreviewed
unsafe. Workspace crates forbid it except for the reviewed operations inatune_py(inventory). Any widening needs a soundness argument and focused tests. - Do not panic to report a library failure. Return
Resultand usethiserror;unwrap/expectonly 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. atune_corestays browser-clean. No clock, no filesystem, no threads in what compiles forwasm32; everything that needs an operating system sits behind thesystemfeature.- Determinism is a feature. Every source of randomness is seeded and derived, every iteration order is stable.
- Public items are documented, and doc examples compile.
- 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-featurescargo clippy --locked -p atune_core --all-targets --no-default-features -- -D warningscargo check --locked -p atune --all-targets --no-default-featurescargo check --locked -p atune_core --target x86_64-pc-windows-msvcandcargo check --locked -p atune --target x86_64-pc-windows-msvc, then the same two with--target aarch64-apple-darwin: default features, so thesystemplatformcfgpaths compile for Windows and macOS (neither crate enablessqliteby default)cargo audit --deny warnings --ignore RUSTSEC-2024-0436, over every package inCargo.lock(cargo denysees only its configured targets), with the pinnedcargo-auditin the workflow'sCARGO_AUDITpip-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-targetsexcludes doctests. Gates 3 and 4 are two commands because one of them cannot do both jobs.--no-default-featuresmust 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_PRELOADinterferes with a command, run it withenv -u LD_PRELOAD; the block above already does. - Give concurrent Cargo sweeps separate
CARGO_TARGET_DIRvalues. - 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:
- One arm of a pair changed. Change the other to the same operation order,
with no fused multiply-add on either side (
f64::mul_addhas no pre-3.13 Python counterpart). - A sampler, scheduler, or seed derivation changed on purpose. Re-bless with
ATUNE_BLESS_PARITY=1 cargo dev check-parity --rust-goldenand read the golden diff; it is the record of the behavior change. - Only
best.valuediffers in the last bits. Run both arms on one machine:f64::cosandmath.cosuse the platform libm, and the goldens name the platform they were blessed on. - The trial count differs. A history-dependent sampler or scheduler is running in parallel; parity examples using one run with parallelism one.
- 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¶
- Guide pages stay short: about 200 hand-written lines, then split or delegate to a reference page.
- 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. - A page is one file plus one
nav:line inmkdocs.yml; either alone fails the build. - An example is one file per language plus its
parity.tomlentry. docs/requirements.inis capped at four direct dependencies. Bounds on transitive packages go indocs/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 |