Skip to content

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

  1. Prep: just release-prep X.Y.Z sets the version in pyproject.toml, regenerates CHANGELOG.md with git-cliff from the squash-merged PR titles, and runs just bench to verify the absolute perf budgets on developer hardware (the CI perf gate is relative to main; the absolute budgets are release-gated here).
  2. Release PR: open a PR titled exactly release: vX.Y.Z; it passes every required gate like any other PR, then squash-merge it.
  3. 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; run just fix to apply autofixes.
  • Google docstrings on every public object, each with a runnable Examples: block (doctests run in CI).
  • Rust: cargo clippy --all-targets -- -D warnings with pedantic enabled (allow-list documented in rust/Cargo.toml); unsafe only at the FFI boundary with // SAFETY: comments.
  • Comments are evergreen; no commented-out code; no TODOs without a linked issue number.