Transcendentals go through libm, not std. IEEE 754 pins the basic operations and sqrt but says nothing about exp/log/erf, and std delegates to the system math library — measured, f64::exp and libm::exp disagree on 9.7% of inputs by one ULP. Since inference is an iterative fixed point, one ULP can change an iteration count.
Three production sites violate it. Each was confirmed to sit outside its file's #[cfg(test)] boundaries — margin.rs has test blocks at 61 and 96, and cavity_log_evidence at 80-95 falls between them.
site
call
measured std-vs-libm divergence on this host
src/factor/margin.rs:84
cavity.sigma().hypot(sigma)
12.136% of 1e6 inputs differ, max 1 ULP
src/factor/margin.rs:92
f64::MIN_POSITIVE.ln()
3.437% of 1e6 inputs, max 1 ULP
src/factor/trunc.rs:98
f64::MIN_POSITIVE.ln()
same
hypot is the material one
It is on the production path for every scored event, and its divergence rate of 12.1% is higher than the 9.7% the rule cites for exp as its own justification. libm::hypot exists and is a drop-in.
Worth being clear that the hypotchoice is correct and should stay — the comment above it explains exactly why, and it was measured: hypot(1e200, 1.0) = 1e200 where naive sqrt(a*a + b*b) gives inf, and hypot(1e-200, 1e-200) = 1.414e-200 where naive gives 0. This issue is only about which hypot.
The two ln calls
f64::MIN_POSITIVE.ln() happens to agree bit-for-bit with libm::log(f64::MIN_POSITIVE) on this host — 0xc086232bdd7abcd2 both ways — so there is no measured effect here. But that is precisely the platform dependence the rule exists to remove: it is a constant, evaluated at runtime through the system libm, and nothing guarantees the same bits on a different target. Both are trivially libm::log(f64::MIN_POSITIVE), or better, a const computed once.
Everything else is clean
src/lib.rs and src/quadrature.rs production code route through libm correctly; the .exp()/.ln() hits there are all inside #[cfg(test)], which the rule explicitly permits. src/predict.rs:512 (hypot) and src/acquisition.rs:228 (ln) are also test-only.
Worth adding
A lint or a test that greps for \.(exp|ln|log|powf|hypot|erf|erfc)\( outside #[cfg(test)] would make this self-enforcing. The rule is stated clearly in CLAUDE.md and was still violated in three places, which suggests prose is not sufficient to hold it.
Found by a floating-point audit, 2026-09-09. Sites confirmed by reading; divergence rates from the audit's measurement.
CLAUDE.md's invariant:
> **Transcendentals go through `libm`, not `std`.** IEEE 754 pins the basic operations and `sqrt` but says nothing about `exp`/`log`/`erf`, and `std` delegates to the *system* math library — measured, `f64::exp` and `libm::exp` disagree on 9.7% of inputs by one ULP. Since inference is an iterative fixed point, one ULP can change an iteration count.
Three production sites violate it. Each was confirmed to sit outside its file's `#[cfg(test)]` boundaries — `margin.rs` has test blocks at 61 and 96, and `cavity_log_evidence` at 80-95 falls between them.
| site | call | measured std-vs-libm divergence on this host |
|---|---|---|
| `src/factor/margin.rs:84` | `cavity.sigma().hypot(sigma)` | **12.136%** of 1e6 inputs differ, max 1 ULP |
| `src/factor/margin.rs:92` | `f64::MIN_POSITIVE.ln()` | 3.437% of 1e6 inputs, max 1 ULP |
| `src/factor/trunc.rs:98` | `f64::MIN_POSITIVE.ln()` | same |
## `hypot` is the material one
It is on the production path for **every scored event**, and its divergence rate of 12.1% is *higher* than the 9.7% the rule cites for `exp` as its own justification. `libm::hypot` exists and is a drop-in.
Worth being clear that the `hypot` **choice** is correct and should stay — the comment above it explains exactly why, and it was measured: `hypot(1e200, 1.0) = 1e200` where naive `sqrt(a*a + b*b)` gives `inf`, and `hypot(1e-200, 1e-200) = 1.414e-200` where naive gives `0`. This issue is only about which `hypot`.
## The two `ln` calls
`f64::MIN_POSITIVE.ln()` happens to agree bit-for-bit with `libm::log(f64::MIN_POSITIVE)` on this host — `0xc086232bdd7abcd2` both ways — so there is no measured effect here. But that is precisely the platform dependence the rule exists to remove: it is a constant, evaluated at runtime through the system libm, and nothing guarantees the same bits on a different target. Both are trivially `libm::log(f64::MIN_POSITIVE)`, or better, a `const` computed once.
## Everything else is clean
`src/lib.rs` and `src/quadrature.rs` production code route through `libm` correctly; the `.exp()`/`.ln()` hits there are all inside `#[cfg(test)]`, which the rule explicitly permits. `src/predict.rs:512` (`hypot`) and `src/acquisition.rs:228` (`ln`) are also test-only.
## Worth adding
A lint or a test that greps for `\.(exp|ln|log|powf|hypot|erf|erfc)\(` outside `#[cfg(test)]` would make this self-enforcing. The rule is stated clearly in CLAUDE.md and was still violated in three places, which suggests prose is not sufficient to hold it.
Found by a floating-point audit, 2026-09-09. Sites confirmed by reading; divergence rates from the audit's measurement.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
CLAUDE.md's invariant:
Three production sites violate it. Each was confirmed to sit outside its file's
#[cfg(test)]boundaries —margin.rshas test blocks at 61 and 96, andcavity_log_evidenceat 80-95 falls between them.src/factor/margin.rs:84cavity.sigma().hypot(sigma)src/factor/margin.rs:92f64::MIN_POSITIVE.ln()src/factor/trunc.rs:98f64::MIN_POSITIVE.ln()hypotis the material oneIt is on the production path for every scored event, and its divergence rate of 12.1% is higher than the 9.7% the rule cites for
expas its own justification.libm::hypotexists and is a drop-in.Worth being clear that the
hypotchoice is correct and should stay — the comment above it explains exactly why, and it was measured:hypot(1e200, 1.0) = 1e200where naivesqrt(a*a + b*b)givesinf, andhypot(1e-200, 1e-200) = 1.414e-200where naive gives0. This issue is only about whichhypot.The two
lncallsf64::MIN_POSITIVE.ln()happens to agree bit-for-bit withlibm::log(f64::MIN_POSITIVE)on this host —0xc086232bdd7abcd2both ways — so there is no measured effect here. But that is precisely the platform dependence the rule exists to remove: it is a constant, evaluated at runtime through the system libm, and nothing guarantees the same bits on a different target. Both are triviallylibm::log(f64::MIN_POSITIVE), or better, aconstcomputed once.Everything else is clean
src/lib.rsandsrc/quadrature.rsproduction code route throughlibmcorrectly; the.exp()/.ln()hits there are all inside#[cfg(test)], which the rule explicitly permits.src/predict.rs:512(hypot) andsrc/acquisition.rs:228(ln) are also test-only.Worth adding
A lint or a test that greps for
\.(exp|ln|log|powf|hypot|erf|erfc)\(outside#[cfg(test)]would make this self-enforcing. The rule is stated clearly in CLAUDE.md and was still violated in three places, which suggests prose is not sufficient to hold it.Found by a floating-point audit, 2026-09-09. Sites confirmed by reading; divergence rates from the audit's measurement.