Contributing to rocci¶
Dev setup¶
Install rustup (the pinned toolchain in
rust-toolchain.toml installs automatically on first build), uv,
and just (cargo install just). Then:
just setup # uv sync + maturin develop --release + pre-commit install
just test # fast suite
Windows note: the Rust MSVC toolchain requires the MSVC Build Tools ("Desktop development with C++" workload from the Visual Studio installer).
Everyday commands¶
justfile is the single answer to "how do I run X" — CI calls the same
recipes. Highlights:
- just test-all (full matrix including a ROCCI_BACKEND=numpy pass)
- just rust-test
- just bench
- just lint
- just fix
- just typecheck
Backend override¶
ROCCI_BACKEND=numpy forces the pure-NumPy kernel;
ROCCI_BACKEND=rust forces the Rust core (raises if the extension is
missing).
The two backends agree statistically but produce different RNG streams,
so bands are not bit-identical across backends at the same seed.
Slow statistical suite¶
pytest -m slow runs the heavy statistical tests (cross-backend
distributional agreement at B=8000 and, the calibration suite).
just test excludes them;
just test-all and CI run
everything.
Golden-master fixtures¶
tests/fixtures/golden/ holds band outputs recorded once from the validated
research implementation (recording details:
tests/fixtures/golden/PROVENANCE.md), and tests/test_golden_master.py
requires every build to reproduce them to atol=1e-6. They are the ground
truth for the band assembly, and the precedence rule is absolute: if code and
fixture disagree, the fixture wins — never regenerate fixtures to make new
code pass. A fixture changes only through a PR that explains the intended
statistical delta and why coverage is preserved, with maintainer sign-off;
just fixtures then re-records them from a checkout of
studroc_paper, the method
validation study the fixtures come from.
Branch model & commits¶
Trunk-based. main is protected: every CI job is a required check, linear
history, no direct pushes. Short-lived feature branches (hours to days),
squash merge. CHANGELOG.md is generated by git-cliff at release time —
don't hand-edit it.
Branch names¶
Name the branch <type>/<short-slug>, where <type> is the Conventional
Commit type the eventual PR title will carry: fix/ingest-bool-pos-label,
test/floor-oracles, ci/gates-workflow. Picking the type when you branch
forces the scope decision early: if you can't name a single type, the task
is really two branches.
PR titles¶
The squash merge makes your PR title the sole commit message on main, so it
must follow Conventional Commits. CI
enforces it against this pattern (note the lowercase type):
^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|release|revert)(\(.+\))?!?: .+
- Type is one of the eleven above. An optional
(scope)narrows it (fix(ingest): ...); a trailing!(feat!: ...) flags a breaking change. - Examples:
feat: add roc_band_ovr for one-vs-rest bands,fix(ingest): treat bool pos_label as a positive class,docs: structure the API reference.
Keep up with main by rebasing, not merging¶
If main moves under you, rebase your branch onto it (git fetch && git
rebase origin/main).
Releasing¶
The tag is the single trigger; everything downstream is machine-checked so a release cannot ship half-done or inconsistent. The human's job is three commands and one approval click.
Per release¶
- Prep:
just release-prep X.Y.Zsets the version inpyproject.toml, regeneratesCHANGELOG.mdwith git-cliff from the squash-merged PR titles, and runsjust benchto verify the absolute perf budgets on developer hardware (the CI perf gate is relative to main; the absolute budgets are release-gated here). - Release PR: open a PR titled exactly
release: vX.Y.Z; it passes every required gate like any other PR, then squash-merge it. - Tag: annotated, on the merge commit, tag name =
v+ the version:
git tag -a vX.Y.Z -m "rocci X.Y.Z" && git push origin vX.Y.Z
Tags are the only publish trigger; workflow_dispatch on the wheels
workflow is a dry-run that builds and smoke-tests but cannot publish.
4. Watch the pipeline (wheels.yml): release-guard asserts tag =
pyproject.toml version = top changelog section, commit on main, tag
annotated — any mismatch aborts before a wheel is built. The matrix then
builds six wheels + the sdist and smoke-tests each in a clean
environment on its own platform (the musl wheel inside an Alpine
container; the sdist by compiling from source).
5. Approve: the run pauses at the release environment. Approving it
publishes — vX.Y.ZrcN tags to TestPyPI, final tags to PyPI — with
provenance attestations attached.
6. Hands off from here: the pipeline polls the index until the version
is live, re-installs it in clean environments on Linux/macOS/Windows and
re-runs an end-to-end band, then creates the GitHub Release from the
changelog section (which triggers the Zenodo DOI). The docs workflow
deploys the versioned site and moves the stable alias.
A failed run before the approval gate publishes nothing and is safe to
retry: fix, merge, and move the tag to the fixed commit
(git push origin :refs/tags/vX.Y.Z then re-tag and push).
Code guidelines¶
- Lint/format: ruff. Type check: ty. Config lives in
pyproject.toml; runjust fixto apply autofixes. - Google docstrings on every public object, each with a runnable
Examples:block (doctests run in CI). - Rust:
cargo clippy --all-targets -- -D warningswith pedantic enabled (allow-list documented inrust/Cargo.toml);unsafeonly at the FFI boundary with// SAFETY:comments. - Comments are evergreen; no commented-out code; no TODOs without a linked issue number.