ADR 0004 — Kani-Verified Invariants
- Status: Accepted
- Date: 2026-07-05
- Phase: 13 (per
docs/IMPLEMENTATION-PLAN-v0.1.0.md) - Deciders: repository maintainers
- Related: ADR 0001 (Loom-verified ring buffer) — same invariant surface, orthogonal exploration. ADR 0003 (property tests) — same invariants, weaker (statistical) exploration.
Context
Three narrow invariants underpin correctness of the workspace’s public surface:
LogLevel::from_numericandto_numericare inverses on[0, 10]. Every downstream comparison, filter, and deserialisation depends on this bijection.LogLevel::from_numericreturnsNonefor values outside[0, 10]. No silent fallback, no wrap.- The session-ID counter’s
fetch_add(1, AcqRel)produces distinct successive values. Downstream aggregation (rlg-mcp::filter_log,rlg-report) trusts this.
Proptests (ADR 0003) validate these statistically. Kani proves
them exhaustively by symbolic execution — every representable
u8 for the numeric bijection, every valid start value for the
counter — in ~seconds per proof.
Decision
Adopt Kani (0.55+) as the model-checked prover for these three
invariants. Author the harnesses in a #[cfg(kani)]-gated module
at crates/rlg/src/kani_proofs.rs, wired from lib.rs with
#[cfg(kani)] mod kani_proofs;. cargo kani sets --cfg kani
automatically; standard builds never compile the module.
Kani runs via the official model-checking/kani-github-action@v1
GHA action on:
- Push to
main— verifies every merge that touches invariant surfaces. - Weekly cron (Monday 06:00 UTC) — catches regressions in Kani’s own upstream (nightly-tracked model checker).
workflow_dispatch— on-demand for maintainers.
Kani is not run per-PR. It is heavyweight (~10 min per proof in current sizing), and its guarantees do not accrete faster than per-merge. Miri, Loom, proptest, and semver-checks carry the per-PR correctness surface.
The three proofs
-
from_numeric_round_trip_matches_to_numeric— for everydisc: u8withdisc <= 10,LogLevel::from_numeric(disc).unwrap().to_numeric() == disc. Proves the bijection. -
from_numeric_returns_none_for_out_of_range— for everydisc: u8withdisc > 10,LogLevel::from_numeric(disc)isNone. Proves the guard clause is exhaustive. -
atomic_fetch_add_yields_distinct_ids— for anystart: u64bounded away fromu64::MAX, two successiveAtomicU64::fetch_add(1, Ordering::AcqRel)calls yield(start, start + 1)and the post-fetch counter equalsstart + 2. Proves the monotonicity contract the session counter relies on.
What Kani does NOT cover here
- Ring-buffer concurrency. Loom (ADR 0001) covers producer / flusher interleavings under exhaustive scheduler exploration. Kani’s concurrency model is single-threaded — the atomic proof above is sequential-only.
u64::MAXwraparound. Bounded away bykani::assume. The practical invariant is what matters; wraparound is unreachable at ~500-year fetch_add rates.- String parsing.
LogLevel::from_strinvolvesto_uppercase()allocation, which Kani struggles to model. Property tests (ADR 0003) carry that coverage. ArrayQueuepush semantics. Third-party trusted; verified upstream bycrossbeam-queue’s own test suite.
Consequences
-
CI cost. Weekly + on-merge. Two Kani jobs at ~10 min each = ~20 min per week. Negligible.
-
Contributor cost. Local reproducer:
cargo install --locked kani-verifier cargo kani setup cd crates/rlg && cargo kani --testsDocumented in
CONTRIBUTING.md. -
Toolchain pinning. Kani ships its own rustc build. This is contained to the
kanijob; the rest of CI runs on stable. -
False positives. Kani occasionally reports issues from upstream (nightly-tracked). Weekly cron catches drift; failures file GitHub issues automatically per the action’s default.
Alternatives considered
- Prusti / Creusot — richer contract language but weaker ergonomics on stable Rust. Rejected for v0.1.0.
- TLA+ spec of the ring buffer — over-budget for v0.1.0 (see plan §6, “Out of scope”). Loom provides the practical guarantee.
- Skip Kani entirely — proptest gives good statistical coverage. But the numeric bijection is trivially amenable to exhaustive proof, and shipping “verified” as a workspace claim requires an actual verifier in the loop. Kani is that verifier.
References
- Kani book
- Kani’s
AtomicU64support matrix: https://model-checking.github.io/kani/rust-feature-support.html - ADR 0001 (Loom) and ADR 0003 (proptest) in this directory.