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 atune_py, which sets unsafe_code = "deny" and documents its four hand-written binding operations at the operation sites. Any widening needs a soundness argument and focused tests.
  2. No panics on library paths. Result and thiserror; unwrap/expect only in tests or provably-infallible spots that carry a comment saying why.
  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
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

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.

The fast tier is slightly wider than the table: CI's per-push jobs also run cargo dev check-api-links (after a fresh cargo doc), cargo dev check-lints, cargo dev check-guide-links, cargo deny check advisories bans licenses sources (pinned cargo-deny), the facade's no-default leg, and the GUI's wasm32 check. Every one is an ordinary local command; they sit outside the table only because the fifteen above are the ones every change owes, while these fire on the subsystems they guard.

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.

Hook revisions in .pre-commit-config.yaml are frozen to full upstream commit IDs; the comments retain the approved release labels. To refresh one, select the approved release tag and inspect its first-party ref before editing:

git ls-remote --tags <repository-url> refs/tags/<tag> 'refs/tags/<tag>^{}'

Use the tag line for a lightweight tag or the ^{} line for an annotated tag, then corroborate the resulting commit on the repository's release or commit page. Update only that rev and its release comment, and run pre-commit validate-config plus the affected hooks on disposable fixture files, using a read-only/check mode where the hook supports one. Do not run pre-commit autoupdate, choose a newer release, or change hook entries as part of a pin refresh; a version or behavior change needs separate 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 ownership tests' child processes on the wheel interpreter. PYTHONPATH= removes explicit path injection, but is not by itself a complete shadowing check: the child processes use isolated -I mode from temporary directories outside the checkout, and an explicit package-location assertion rejects a source-tree import. The canonical-analysis tests copy their immutable journal fixtures before opening them, so the source fixtures remain unchanged.

Python ownership safety runtimes

These checks complement the installed-wheel gate; they do not replace it or add a supported release wheel. Record the source identity, interpreter and toolchain versions, exact command, test count and diagnostics. A successful --no-run build is preparation, not safety execution evidence. Missing prerequisites and open gaps belong in the repository's MASTER_STRATEGIC_PLAN.md (not published on this site).

For Linux x86_64 AddressSanitizer, use pinned nightly-2026-07-26 with rust-src, and a CPython interpreter with its development library and the declared NumPy dependency. Replace the interpreter placeholder with that environment's lexical executable path:

rtk proxy env -u LD_PRELOAD \
  CARGO_TARGET_DIR=/tmp/atune-python-asan CARGO_BUILD_JOBS=2 \
  PYO3_PYTHON=/absolute/path/to/python RUSTFLAGS=-Zsanitizer=address \
  cargo +nightly-2026-07-26 test --locked -Zbuild-std \
  --target x86_64-unknown-linux-gnu -p atune_py --lib -- --test-threads=1

Keep extension-module off: this builds a Rust test executable that embeds Python and links Rust's matching sanitizer runtime. The explicit target keeps host build scripts and procedural macros separate; -Zbuild-std instruments the Rust standard library. CPython, NumPy and other native dependencies are not instrumented by this command, so it is not whole-process memory-safety proof. Preserve sanitizer reports; do not silently suppress a failure or turn off leak detection to obtain a green result. See the Rust sanitizer guide.

The embedded test executable above is complementary evidence, not the release acceptance test for installed Python shutdown. The installed-wheel safety gate must run fixed-work create/run/drop cycles, release the complete study between cycles, and verify Python-object release and bounded retained-memory growth after warmup. Cover callback and objective failures, stop, abandoned trials, queue ownership, parallel workers, and NumPy exports. Retained trial history and peak RSS alone do not establish a leak. Keep normal-allocator coverage as well as a separate sanitizer-compatible run.

For that separate run, use an isolated pinned CPython build with ASan and --without-pymalloc, instrument the Rust extension, and verify that both use one compatible sanitizer runtime. Do not replace the host interpreter. Preserve raw unsuppressed CPython-only, NumPy-only, and installed-atune reports with exact source, compiler, options, dependency and artifact identities. An accepted teardown signature needs independently reproduced, reviewed evidence and explicit allocation/byte bounds; new signatures, growth, address errors, truncated reports, or changed identities fail. Never subtract aggregate leak totals or suppress a whole library. Deliberate native leak, invalid-access, and Python retained-object controls must fail through the same acceptance path. A clean build or a clean uninstrumented execution cannot replace them. See the CPython sanitizer build guidance and LeakSanitizer documentation.

The fixed-work probe is crates/atune_py/tests/safety/lifecycle.py. For the pinned ordinary CPython 3.10 wheel environment, run it from a disposable working directory, with an absolute path to the probe and evidence outside the checkout:

rtk proxy env -u LD_PRELOAD -u PYTHONHOME -u PYTHONPATH -u PYTHONMALLOC \
  OPENBLAS_NUM_THREADS=1 timeout 120 /absolute/path/to/wheel-venv/bin/python -I \
  /absolute/path/to/atune/crates/atune_py/tests/safety/lifecycle.py \
  --allocator pymalloc --cycles 64 --warmup 8 \
  --output /absolute/path/to/evidence/lifecycle.json

Its short unit-test runs check the probe interface, not a long-lived-use certificate. Keep both --negative-control and --negative-growth-control results: each must exit nonzero for its specific retained-object or memory-growth reason. Python weak references cannot observe every native extension handle; the instrumented native gate remains independently required.

Repeat the traced run with PYTHONMALLOC=malloc, --allocator malloc, and -s instead of -I: isolated Python ignores PYTHONMALLOC. Continue clearing PYTHONHOME/PYTHONPATH and verify installed-package origins in the report. For the separately instrumented no-pymalloc interpreter, use --allocator sanitizer --memory-observer none. That mode requires actual ASan configuration and mapped-runtime evidence, executes the same ownership cases, and reports traced growth as unavailable; both traced normal/malloc runs remain required. CPython's allocation profiler itself produces shutdown leaks in the selected diagnostic runtime, so native and traced-growth observations are kept separate. Historical profiler reports remain evidence, not suppressed passes.

.github/release/build_python_safety_runtime.sh prepares the isolated diagnostic interpreter. It pins the CPython source archive, checks the selected Clang major for both C and C++ compilers, limits parallelism, records build/runtime identities and exercises native controls. Select an explicitly installed matching Clang with --cc and --cxx; if that private compiler requires shared libraries, select its one absolute directory through --compiler-library-path, not ambient LD_LIBRARY_PATH. use --output only with an absent or empty disposable directory. Its successful exit proves preparation and controls, not the installed-atune lifecycle. Build and package-install bootstrap may disable leak checking, but acceptance runs must enable it. Do not set LSAN_OPTIONS=exitcode=0: with the selected runtime that also makes an address error exit zero.

For allocation-origin investigation only, --disable-float-freelist adds the fixed upstream CPython setting -DPyFloat_MAXFREELIST=0 to this disposable runtime's compiler flags. The default remains unchanged. Metadata records the choice; separately verify actual free-list statistics after allocating and releasing dynamic floats. This prevents float-object reuse from obscuring the original malloc call site, but neither proves a leak harmless nor replaces normal-allocator growth checks. A new runtime needs fresh identity-bound captures and independent negative controls; never reuse an old leak baseline.

Classify preserved captures with .github/release/check_python_safety.py:

rtk proxy python .github/release/check_python_safety.py \
  --log /absolute/path/to/evidence/raw.stderr --exit-status 1 \
  --identity /absolute/path/to/evidence/identity.json \
  --baseline /absolute/path/to/evidence/reviewed-baseline.json \
  --completion /absolute/path/to/evidence/completion.json \
  --stdout /absolute/path/to/evidence/raw.stdout

Use the actual captured status, never a substituted workload exit code. A clean report requires status zero. A complete leak-only report may retain the selected runtime's status one only with a reviewed exact-stack baseline and separate completed-workload evidence. Address errors, other statuses and missing or mismatched evidence fail. Identity schema atune-python-safety-identity/v2 binds source archive, compiler, ASan runtime/options/build flags, wheel, NumPy and workload attestation; baseline/completion schemas are version 1. Completion binds the identity, raw stderr/stdout digests, status and workload evidence. Control references require immutable digests and explicit independent review. The offline checker does not fetch or authenticate external review artifacts: their independence and scientific adequacy remain the reviewer's responsibility. It never generates an accepted baseline; an empty baseline accepts no leaks. The pinned acceptance options are ASAN_OPTIONS=detect_leaks=1:halt_on_error=1:abort_on_error=0:fast_unwind_on_malloc=0, with LSAN_OPTIONS unset. Record those canonical environment keys in runtime.options; use JSON null for the unset value. Other sanitizer options require a separate reviewed policy change, not a baseline-file workaround. Slow allocation unwinding is required because the default frame-pointer unwinder produced unresolved frames through the pinned NumPy binary. Preserve every reported frame, module offset and build identity; unresolved signatures remain ineligible for a baseline. Retain effective-option evidence and rerun clean, deliberate-leak and invalid-access controls with the selected settings. The runtime builder's standalone preparation controls use their own minimal detection settings; they are not captures eligible for this acceptance policy.

For the separate Linux free-threaded safety runtime, build a local wheel with the installed Maturin and an explicitly selected free-threaded CPython interpreter:

rtk proxy env -u LD_PRELOAD CARGO_BUILD_JOBS=2 maturin build --locked \
  --manifest-path crates/atune_py/Cargo.toml \
  --interpreter /absolute/path/to/python3.14t --compatibility linux \
  --target-dir /tmp/atune-python-free-threaded \
  --out /absolute/path/to/empty-wheel-output

Inspect the wheel's WHEEL metadata: the safety artifact must be cp314-cp314t-*, not cp310-abi3-*. PyO3 ignores the limited-API setting for this interpreter and builds a version-specific extension; this does not change atune's release matrix. See the PyO3 free-threading guide.

Install that exact artifact with --no-deps into a disposable free-threaded environment outside the checkout. Verify its interpreter version, sysconfig.get_config_var("Py_GIL_DISABLED") == 1, installed package path, and not sys._is_gil_enabled() after importing atune. CPython documents the runtime GIL check. An existing pytest environment may orchestrate the ownership scenarios even when pytest is unavailable for the free-threaded interpreter:

rtk proxy env -u LD_PRELOAD PYTHONPATH= \
  ATUNE_TEST_PYTHON=/absolute/path/to/free-threaded-venv/bin/python \
  /absolute/path/to/pytest-venv/bin/python -m pytest \
  crates/atune_py/tests/test_installed_wheel.py -q

Each scenario runs in a fresh isolated child using ATUNE_TEST_PYTHON and checks package origin and that the free-threaded GIL stays disabled on import. This suite exercises queue/session ownership, exceptions and object release; it does not exercise NumPy. Missing free-threaded NumPy blocks its own runtime coverage, not these dependency-independent ownership scenarios.

Three tiers, and which one you owe

The fifteen gates above are the fast tier: everything a change is expected to pass before it is proposed, on any machine, with no special hardware. Two more tiers exist, and confusing them is how a suite ends up either untrustworthy or unrunnable.

Tier What is in it Who runs it, and when What a failure means
fast the fifteen gates above, plus the CI extras listed after the traps every change, locally and in the release_gate and wasm CI jobs a regression; fix before proposing
slow the #[ignore]d GP sweep and the heavy benchmark oracles — kept out of the fast loop by their cost, not by their prerequisites the slow CI job, on every push to main and on demand; locally with cargo test -p atune_bench --release -- --ignored a real regression: nothing here depends on a machine outside the runner
live the environment-gated suites — the journal's real shared filesystem case (ATUNE_NFS_DIR) and the four real-Oniro cases (ONIRO_CLI) the live CI job, dispatched manually onto a runner that actually has both; never on a schedule either a real regression or a changed external contract — read the retained logs before deciding which

The live tier is manual on purpose. An ordinary CI runner does not necessarily have a shared filesystem or an Oniro binary, so a scheduled run could fail for the wrong reason every time and teach everyone to ignore it. The job refuses to start without both prerequisites rather than reporting a pass it did not earn. The self-hosted runner supplies ATUNE_NFS_DIR, ONIRO_CLI, ONIRO_REPO, and GYMNASIUM_RS_REPO; the dispatch supplies the full expected Oniro and gymnasium_rs commits. The preflight requires a Linux nfs or nfs4 mount and clean sibling trees at those commits, then retains the environment manifest, explicit exit codes, logs, and Oniro run roots for ninety days. An environment-gated claim is only worth the evidence that survives the machine that produced it.

The exact live commands are ATUNE_NFS_DIR=/absolute/nfs/scratch cargo test --locked -p atune_core --test journal_nfs -- --ignored --nocapture and ONIRO_CLI=/absolute/oniro cargo test --locked -p atune_oniro --test oniro_spike -- --ignored --nocapture. Local substitutes do not close these evidence rows.

Evidence outside local gates

Local green output does not prove these surfaces:

Surface Prerequisite Required evidence
Real NFS Linux; readable /proc/self/mountinfo; an absolute, canonicalizable, writable ATUNE_NFS_DIR whose filesystem type is exactly nfs or nfs4 Mount identity, commit, six-worker × 25-trial ignored-test log, explicit exit code
Real Oniro Unix; executable ONIRO_CLI; clean Oniro and gymnasium_rs trees at explicitly expected commits Binary SHA-256, sibling commits/status, supported schema-1/schema-2 four-test log, explicit exit code, retained roots
Self-hosted platform matrix Linux x86_64/aarch64, macOS x86_64/aarch64, and Windows x86_64 native self-hosted runners Full gate logs, five native wheel installs, and production manylinux_2_17 proof
Hosted CI/Pages Maintainer-enabled protected runs; final repository/site identity; Pages source set to GitHub Actions; github-pages environment allows the authorized default branch or release tag Run IDs, immutable complete-version artifact, deployment commit, and public URL checks
Fresh advisories An online isolated advisory index Exact cargo-deny version, index revision, lockfile, and full standard-fetch report; protected rerun remains a release gate
Crate/wheel/sdist artifacts Locally buildable artifacts Isolated consumer installs/imports and a deterministic manifest
Publication/signing/rollback Explicit maintainer authorization Registry receipts, signatures or provenance, and a staging rollback rehearsal

All CI execution requires explicit self-hosted routing. Native jobs retain their OS/architecture requirements; missing runners are unavailable evidence, not permission to fall back to GitHub-hosted machines or Linux-only checks. Cross-compilation proves a target build, not native wheel imports or platform permissions. Linux manylinux wheel jobs additionally require docker-build and a successful Docker preflight. Fork pull requests cannot execute on this persistent fleet. Canonical Pages deployment still requires an eligible runner and workflow in the canonical repository; private SpaceRL CI does not supply cross-repository Pages identity or OIDC authority.

Gate 15 needs a nightly toolchain at or above the workspace's Rust version, and runs without -D warnings deliberately: docs.rs reports warnings and publishes anyway, so the gate asserts what docs.rs will actually do — the build succeeds — and adding the flag would couple it to nightly's evolving lint set. Lint enforcement lives in gate 8, whose stable rustdoc runs -D warnings on both feature sets.

The parity gate

Gate 13 is unusual enough to need its own page: the parity gate — what bit-for-bit equality asserts, how to run both halves, the five causes of a red in the order to check them, and what you may not do to make it quiet.

Generated pages are not edited

Seven pages under reference/, the three Python type stubs, and docs/api_links.yml are written by cargo dev generate-all, from the code's own registries: the clap command tree, the [features] tables, the error enum, the sampler and scheduler catalogues. They carry begin/end pragmas, and gate 9 fails when a checked-in file has drifted.

So: never hand-edit a generated page. Change the code, then run cargo dev generate-all --mode write and commit both. Adding a new generated page is one GeneratedDoc implementation plus one line in the driver — no documentation edit is needed for a new sampler, flag or feature, which is the point.

Documentation rules

The guide is held to five rules, and they are what keep it from rotting:

  1. No page over 200 lines of hand-written source. A page that outgrows it splits, or delegates to a reference page.
  2. No code block in a page. Every fenced block is a macro call that pulls a real file out of examples/, so a page cannot contain code that CI does not run. Short literals — cargo add atune, a manifest line — go in a code span instead.
  3. Adding a page is one file plus one nav: line in mkdocs.yml. A page that is not in the nav fails the build, and so does a nav entry with no page.
  4. Adding an example is one file per language, plus its parity.toml entry.
  5. docs/requirements.txt names four direct dependencies, and that is a cap. Every addition is a maintenance liability and a supply-chain edge.

Build the site with uv run --with-requirements docs/requirements.txt mkdocs serve from an environment that has the wheel installed. The internal design corpus under docs/design/ is excluded from the site on purpose: 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 that would drift — the quickstart — is a shared snippet included by both.

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, the 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
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