Skip to content

Versioning and stability

What a version number means

Every crate in the workspace carries the same version, and the internal dependencies declare it (atune_core = { path = …, version = "0.1.0" }). A release is therefore all of them at one number, not four independently versioned crates. The Python wheel is built from the same tree and carries the same version.

The numbers follow cargo's reading of semantic versioning, which before 1.0 is stricter than people expect:

Change Before 1.0 After 1.0
Breaking API change a minor bump — 0.1 → 0.2 a major bump
New capability, nothing removed a patch bump — 0.1.0 → 0.1.1 a minor bump
Bug fix a patch bump a patch bump

So while atune is 0.x, treat every minor bump as breaking. A dependency written as atune = "0.1" will not silently pick up 0.2.

What is stable today

The guarantees are narrower than a general API stability promise:

  • The four plugin seams are frozen in shape, not in signature: a atune::Storage, atune::Sampler, atune::Scheduler or atune::Objective implemented against today's traits is the shape they will keep, and the storage conformance suite is what an out-of-tree backend is held to. See Extending.
  • The on-disk journal has a version field and a compatibility rule. This build writes format version 6; a reader refuses a journal whose header names a higher version rather than guessing at it, and still opens a lower one (a writable open migrates it once). The format is documented field by field on the journal reference and pinned by a committed golden file.
  • Optuna's journal format is a stable import and export target, because Optuna itself guarantees it. See Optuna interop.
  • Determinism is asserted, within a version. See the section below, which is the one stability question that bites people who record results.

The surfaces, and what each one commits to

Surface What it is Pre-1.0 expectation
atune (Rust facade) The crate to depend on The main API surface; outside the pinned subset, anything may change with a minor bump
atune_core The model layer and the four seams Depend on it directly only when implementing a seam
atune_derive #[derive(Space)] Generated code names the facade, so the two travel together
atune_cli (binary atune) The command-line tool Flags and subcommands may change; the CLI reference is generated from the code, so it cannot describe a flag that no longer exists
atune on PyPI The Python package Mirrors the facade's shape rather than its exact API
Cargo features Part of the public API Adding one is additive; removing or renaming one is breaking. The feature reference is generated from the manifests
The journal file The on-disk study Versioned, with the compatibility rule above

The facade subset Oniro builds on

Oniro, the sibling reinforcement-learning framework, tunes through a small part of the Rust facade. That part is declared as contract XC-07, version 1: the paths below keep resolving under the named features, and the calls a downstream crate makes with them keep compiling, for as long as version 1 is supported.

The checked-in list crates/atune/tests/fixtures/xc07-facade-v1.txt names every pinned path, one per line with the feature it needs. It covers the study builder and handle, the objective and reporting context, the multi-seed protocol, search spaces and the DSL parsers, the TPE, Random, and DEHB samplers with the ASHA and PBT schedulers, storage (the journal under system, SQLite under sqlite, and the opaque StateBlob), the subprocess executor and its constants (system), the ask entry and trial attributes, and the PB2 scheduler with the in-run population (pb2).

Within version 1 the contract only grows: a new pinned item, other than a variant of one of the exhaustive enums below, or a signature that accepts more than it did, is an ordinary change. Removing, renaming or moving a pinned item, or changing it so that a call that compiled before no longer does, is a breaking change to the contract. Version 1 is preserved as a historical fixture; it does not require the old API to keep resolving after a version 2 break.

XC-07 version 2 records D137's breaking API migration. D137 also removes the first-party atune::prelude and atune_core::prelude modules; use explicit imports from the facade root or the module that owns each item. The crate-root and module re-exports remain available. Fallible storage and reader constructors now use try_new names: JournalStorage::{open, with_options, open_read_only, open_read_only_with, open_read_only_with_options} map to try_new, try_new_with_options, try_new_read_only, and try_new_read_only_with_options; SqliteStorage replaces open, with_options, open_read_only, open_read_only_with, in_memory, and in_memory_with with try_new, try_new_with_options, try_new_read_only, try_new_read_only_with_options, try_new_in_memory, and try_new_in_memory_with_options. RemoteStorage::connect becomes try_new; connect_observer and its connect_read_only alias become the single try_new_with_observer constructor; serve_embedded becomes try_new_embedded. The role-selection helper remains private. StudyReader::load is replaced by StudyReader::try_new. RestartMode::shrink_perturb and MedianPruner::percentile are replaced by RestartMode::try_new_with_shrink_perturb and MedianPruner::try_new_with_percentile. GP fitting factories likewise changed: Gp::fit, fit_mle, and fit_map are now try_new, try_new_with_mle, and try_new_with_map; TvGp::fit and fit_noisy are now try_new and try_new_with_noise. The affected search-space constructors also use fallible try_new names. There are no compatibility shims for the old names.

Infallible static factories use new or descriptive new_with_* names: Budget::unbounded and Budget::trials became Budget::new and Budget::new_with_trials; RetryPolicy::none and RetryPolicy::times became RetryPolicy::new and RetryPolicy::new_with_max_retries; Mabc::ucb_default and Mabc::ucb became Mabc::new and Mabc::new_with_ucb; and Qmc::halton became Qmc::new_with_halton. SpaceSchema::empty became SpaceSchema::new. Open::{log, linear, around, around_int} became Open::{new_with_log, new_with_linear, new_with_around, new_with_around_int}; TrialTemplate::fixed and TrialTemplate::fork became TrialTemplate::new_with_fixed_params and TrialTemplate::new_with_parent_and_mutation. MemStorage's with_clock factory became new_with_clock. Perturb::factors became Perturb::new_with_factors.

The remaining public named constructor changes are Point::unconstrained to Point::new_with_unconstrained; AshaPruner::from_ladder, MedianPruner::variance_aware, and ReplicateFan::from_tune to new_with_ladder, new_with_variance_aware, and new_with_tune; OutboxEffectIntent::{resume, replacement} and OutboxPageRequest::defaults to new_with_resume, new_with_replacement, and new_with_defaults; and TrialMeta::derive to TrialMeta::new_with_study_seed.

The redundant from_parts aliases on FinalizationToken, FinalizationObservation, TrialReservation, TrialOwnerToken, and LeaseObservation were removed; their existing new constructors remain. OutboxKey now implements From<TrialOwnerToken> by forwarding to OutboxKey::from_token, which remains available.

The DSL parser results are named records: parse_named returns NamedDistribution { name, distribution }, parse_growing returns GrowingDistribution { distribution, open }, and parse_schema_growing plus parse_schema_lines_growing return GrowingSchema { schema, policy }. These records are #[non_exhaustive], so downstream patterns must allow future fields. Evolving Decision, Command, SamplerChoice, diagnosis Rule, and IncumbentPoint are also #[non_exhaustive]; error types carry type-level #[must_use]. Poisoned mutex contents are never inspected or repaired: calls that need the protected state return typed errors, while cleanup skips inaccessible state. These are the owner's one-time D137 authorization for the breaking changes: Oniro must migrate its source to the v2 paths before changing its pin to the v2 producer revision. The next compatible minor release containing this break must be 0.2.0; this note does not change the current workspace version or authorize a release.

The same v2 migration gives ParamWrite named name, distribution, and value fields, while its serde representation stays the historical three-element tuple. Storage::write_params remains tuple-based. The float and integer options constructors accept Scale instead of log: bool; the serialized Distribution variants still carry log: bool. FencedMutation is #[non_exhaustive], so downstream matches need a wildcard arm. Any future mutation that an adapter cannot represent must return Error::Unsupported before applying or sending it. External conversions that can encounter a future variant are fallible with TryFrom. Current mutation wire variants remain unchanged.

Protocol records and vocabularies declared in storage::* intentionally support literal construction and exhaustive matching by backend authors. A future field or variant change requires a coordinated migration of every in-tree backend, wire conversion, conformance suite and affected consumer contract, with an explicit downstream migration handoff before a consumer changes its producer pin. This policy covers the storage API, snapshot, lifecycle, import, outbox and finalization records; it does not make their fields immutable or permit unversioned serialized changes. Evolving algorithm choices and model/report outputs use #[non_exhaustive] instead. In particular, shared trial model records (FrozenTrial, TrialRecord, TrialTemplate, TrialDelta) use their public constructors even when carried by a storage method. Diagnosis records provide constructors for custom renderers and synthetic reports. Fixed scalar IDs, Boolean/geometry vocabularies and statistical coefficient records retain their closed shapes.

The contract is narrower than it may look:

  • It is still a pre-1.0 promise between two repositories with one owner, not the semantic-versioning guarantee 1.0 will make. Everything outside the list may change with a minor bump.
  • Only the listed module paths are pinned. The crate-root aliases (atune::Study, atune::Tpe) are conveniences and may move; the facade and core preludes were removed as part of the XC-07 v2 migration.
  • The pinned enums without #[non_exhaustive] (Direction, TrialState, Distribution, ParamValue, Aggregate, Pairing, RestartMode, StudyLoadOutcome and pb2::Explore) gain no variant and no #[non_exhaustive] within XC-07 v1, since Oniro may match them exhaustively. A variant arrives only with XC-07 v2; see D121 in the plan.
  • The numbers a study produces are not pinned across versions: determinism stays a within-version promise, and the journal keeps its own format version.
  • The environment-variable names the executor sets (ATUNE_PARAM_<name>, ATUNE_TRIAL_NUMBER, ATUNE_SEED, and the rest) and its default prune exit code, 42, are part of the contract. Its stdout protocol is the one described in tune any program.

The contract is checked, not just written down: crates/atune/tests/facade_contract.rs resolves every pinned path at compile time, requires the rendered list to equal the checked-in file byte for byte, and drives each area end to end through those paths only, as a dependent crate would.

Determinism is a within-version promise

Given the same version, seed, closed search space, and stateless sampler, a study reproduces exactly across thread counts and across the Rust and Python surfaces. History-dependent samplers, schedulers, and open search spaces have the narrower replayability contract described in Determinism; a fixed-seed single-worker run remains fully reproducible.

Across versions it is deliberately not a promise. Improving a sampler changes which configuration it suggests, so the byte-for-byte outputs change with it; the repository's parity and journal goldens are re-blessed on purpose when that happens, and the diff is the record of the change. If a result has to be reproducible later, pin the exact version that produced it — a caret requirement is not enough.

An open search space stores its growth policy in the study. A build that predates open search spaces does not read that policy: it samples the declared seed range while earlier trials recorded wider distributions. The compatibility check accepts that bound drift, so such a study degrades — growth stops — rather than corrupting.

Rust version

The workspace sets rust-version = 1.92 and edition 2024, and every published crate inherits it. An older toolchain refuses the build outright rather than failing halfway through it. An MSRV increase is recorded in the changelog as a breaking change, because for a consumer pinned to an older toolchain it is one. The two unpublished GUI crates, atune_gui and atune_showcase, declare rust-version = 1.95 because egui and eframe 0.37 need it (D125); they are not part of this promise.

Python wheel support

Wheels use abi3-py310 for GIL-enabled CPython 3.10 and newer. Free-threaded CPython builds, including cp314t, are not shipped; raising the supported floor is a breaking change.

The publish order, and why it is not one command

The four published crates must go to crates.io bottom-up:

atune_core → atune_derive → atune → atune_cli

This is not a preference. The internal dependencies are path + version entries, and a normal publish package resolves the version half against the registry. Before the first release, an unpatched cargo package -p atune fails with:

no matching package named atune_core found … location searched: crates.io index

The push gate runs a fast package include/exclude check. The dispatch-only release-staging rehearsal packages all four crates bottom-up with temporary same-workspace patches and verifies each archive; authorization and artifact handling are in Staging, authorization, and rollback.

atune_bench, atune_gui, atune_dev, atune_showcase and atune_plugin_example are publish = false and are never released. They are internal tooling, a demo and a compile-checked example; their Rust API reference is published on this site rather than on docs.rs, because they will never appear there.

Staging, authorization, and rollback

The repository's release automation stops at a manual, read-only staging rehearsal. A staging run must build the supported crate, wheel, sdist, and documentation artifacts from one commit; consume each build in an isolated environment; run the current dependency-policy gate; and retain a sorted manifest, SHA-256 sums, a standard SBOM, and available hosted provenance. A successful workflow run is review evidence, not permission to publish.

Publication requires a maintainer to review that evidence, select and protect the release identity or signing mechanism, and authorize the exact version and destinations. Registry credentials, signing keys, GitHub Pages mutation, crate yanks, and PyPI uploads are intentionally absent from the staging workflow. Nothing should be published from a dirty worktree or from artifacts rebuilt after the reviewed staging run.

If a problem is found before publication completes, stop and leave every remaining destination untouched. If an already published artifact is unsafe, the maintainer follows the registry's supported withdrawal mechanism (for example, a crates.io yank), publishes a corrected version rather than replacing immutable bytes, and moves documentation aliases only after the corrected artifacts pass staging. PyPI and crates.io receipts, provenance, signatures, and the incident decision are retained with the release evidence. The local rollback rehearsal exercises only disposable indexes and aliases; it never writes a public registry or production documentation target.

Documentation versions

The site is versioned from the start: /latest/ moves with each stable release, each release also keeps an immutable /0.1.0/-style snapshot, and /dev/ is built from the repository's default branch when the documentation workflow publishes it. Retrofitting that later would rewrite every published URL, so the mechanism is in place before there is anything to publish with it.

Where to go next

If you want to Go to
What changed, release by release Changelog
To contribute, and the gates a change must pass Contributing
What determinism actually guarantees Determinism
Which feature carries which algorithm Feature reference
To report a vulnerability Security policy