Linting and formatting

CI runs these as a gate; run them locally first.

# Rust
cargo fmt --all -- --check
cargo clippy --no-default-features -- -D warnings

# Python
ruff check .
ruff format --check .
mypy python/disarm --ignore-missing-imports

Run CI's ruff, not whichever one is on your PATH. The pre-commit hooks are language: system, so they use the ruff your shell finds, and it is easy for that to be older than the pin. It matters more than a version number usually does: 0.16 formats Python inside Markdown fenced blocks and 0.15 does not, and this repository's docs are full of executable Python in fences. A stale ruff passes ruff format --check . locally and fails the Lint & format job with no hint that a version is involved.

uv pip install "ruff==$(python3 -c "
import re,pathlib
print(re.search(r'ruff==([0-9.]+)', pathlib.Path('pyproject.toml').read_text()).group(1))")"

The pin is written once, in the dev extra; CI's Lint & format job reads it from there. tests/test_toolchain_pins.py asserts that CI keeps no copy of its own and that the ruff you are running matches the pin, so this is caught by pytest rather than by a pull request.

Three gates CI runs that the block above does not

Each of these has sent an avoidable red build. They are listed here because running the core-and-Python commands to the letter is not sufficient to predict CI.

1. cargo doc is run by nobody. No CI job invokes it, so a broken rustdoc link ships to docs.rs unnoticed — six were live at once in August 2026. It is fast, and it is the published API page:

cargo doc --no-deps      # must be warning-free

Note crate:: paths in public docs must point at the crate::api:: re-export, not at the pub(crate) module the item really lives in; rustdoc rejects the latter as a private link.

2. Your clippy is not CI's clippy. CI follows dtolnay/rust-toolchain@… # stable and there is no rust-toolchain.toml pinning the repo, so a local toolchain drifts behind and lints added in the gap cannot fire for you at all. rustup update stable before trusting a -D warnings run.

3. The binding gates are not in the block above. RuboCop, Biome, the Ruby and Node suites, the JVM tests and the C smoke test all run in CI and none is listed anywhere in this file. Every binding builds against the published core, so an unreleased API needs the [patch.crates-io] redirect CI injects — and its location differs per binding. bindings/ruby is a cargo workspace, so a patch appended to bindings/ruby/ext/disarm/ is ignored with only a warning and the build then fails against the published core.

binding append the redirect to path
cabi bindings/cabi/Cargo.toml ../..
node bindings/node/Cargo.toml ../..
ruby bindings/ruby/Cargo.toml (workspace root) ../..
java bindings/java/rust/Cargo.toml ../../..

Run every line from the repo root. Each is a subshell so the cd does not leak into the next one — chaining bare cds here silently runs the second binding's commands inside the first binding's directory.

# Allocation gate on the glue. BINDING is a PATH the script cd's into, not a short name.
BINDING=bindings/node       bash scripts/perf_lint.sh
BINDING=bindings/ruby/ext/disarm bash scripts/perf_lint.sh
BINDING=bindings/cabi       bash scripts/perf_lint.sh
BINDING=bindings/java/rust  bash scripts/perf_lint.sh

( cd bindings/ruby && bundle exec rubocop && bundle exec rake compile && bundle exec rspec )
( cd bindings/node && npx biome check . && npm run build:debug && npm test )
( cd bindings/java && ./gradlew test --offline )

# The C ABI: smoke.c is the ONLY behavioural coverage that crate has — CI never runs
# `cargo test` there, so a Rust #[test] in it would be compiled and never executed.
( cd bindings/cabi \
  && cargo build --release \
  && cc examples/smoke.c -I. -L target/release -ldisarm_ffi -o /tmp/disarm_smoke \
  && LD_LIBRARY_PATH="$PWD/target/release" /tmp/disarm_smoke )

Restore every manifest afterwards (git checkout -- <manifest>). A committed relative-path redirect breaks release packaging.

Skip the binding block only when git status shows no bindings/ file changed and no public src/api signature moved.