Skip to content

Audit remediation: fix all 64 findings from the July 2026 audit - #36

Merged
alexnodeland merged 22 commits into
mainfrom
audit/2026-07-remediation
Jul 13, 2026
Merged

Audit remediation: fix all 64 findings from the July 2026 audit#36
alexnodeland merged 22 commits into
mainfrom
audit/2026-07-remediation

Conversation

@alexnodeland

Copy link
Copy Markdown
Owner

Complete remediation of the July 2026 ecosystem audit (see AUDIT-2026-07.md, included and fully resolved in this PR). All 64 findings are fixed — none deferred — and every fix was independently re-verified by an adversarial verifier against the final code (64/64 verified).

Highlights

Silent-wrong-answer fixes (all four inference engines):

  • MH: restored the missing Hastings/Jacobian term for log-space proposals (FG-02); prior-resample proposals for discrete sites (FG-10); deleted the name-substring proposal heuristics that broke ergodicity (FG-42)
  • SMC: importance weights no longer square the prior (FG-03); adaptive_smc is now genuine likelihood-tempered SMC with an unbiased log-evidence estimate (FG-43/58); rejuvenation no longer re-skews weights (FG-13)
  • VI: scale parameters are actually optimized (FG-04), CRN central-difference gradients (FG-16), support-aware guides with typed errors for discrete latents (FG-17), conjugate-recovery proof test (FG-45)
  • ABC: correct Beaumont/Toni ABC-SMC with weighted perturbation kernels (FG-09), bounded attempts with typed errors (FG-34)
  • Diagnostics: normalized multi-chain ESS and split-R-hat per Vehtari et al. 2021 (FG-01/36/37), autocorrelation-consistent Geweke (FG-39), known-answer tests (FG-35)

Runtime & correctness: stack-safe trampolined interpreter (FG-19), Result-based structure-varying trace scoring (FG-20/21), duplicate-address detection (FG-47), collision-free address encoding (FG-26/52), irrefutable-pattern support in prob! (FG-61)

New capability (FG-31): finite-difference HMC with dual-averaging step-size adaptation, plus 7 new distributions (StudentT, Cauchy, Laplace, Weibull, ChiSquared, InverseGamma, DiscreteUniform) — 17 distributions total, all with scipy-derived known-answer tests and Validate coverage (FG-55)

Performance (FG-05/22/24/62-64): Address is now Arc<str> with a cached hash (O(1) clone); the dead memory-pooling subsystem (CowTrace/TracePool) was benchmarked (<10% win), deleted, and docs de-advertised; benches now measure the real inference entry points

Tests & docs: sampler KS/chi-square validation for every distribution (FG-14), the conjugate-posterior harness is finally called from CI (FG-15), CLT-justified tolerances (FG-49), honest README claims + MSRV pin + CHANGELOG (FG-23/50/51), error-code reconciliation (FG-33)

Verification

  • Final gate: full test suite green (300+ tests incl. new regression tests, 164 doctests), clippy 0 warnings, fmt clean, benches compile
  • 64 adversarial re-verification agents (one per finding, re-deriving math from primary references): 64/64 verified
  • Post-remediation regression review over the entire diff: 3 low-severity issues found, all fixed in follow-up commits
  • Every math fix ships a seeded regression test that fails on the pre-fix code

🤖 Generated with Claude Code

https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2

alexnodeland and others added 22 commits July 10, 2026 21:48
…ng, and add known-answer tests (FG-06, FG-07, FG-08, FG-27, FG-28, FG-29, FG-30, FG-32, FG-53)

Remove bogus overflow guards in Normal/LogNormal (|z|>37), Gamma
(rate*x>700), and Exponential (rate*x>700) — the log-densities are pure
log-space and finite everywhere (FG-07, FG-08, FG-30). Fix Beta boundary
semantics to match scipy (interior computed exactly with no ln cutoffs;
endpoints return the true -inf / finite / +inf limits) (FG-27). Handle
Binomial degenerate p=0/p=1 exactly instead of producing NaN (FG-28).
Cache the Categorical CDF, validate once at construction, sample via
binary search, and index log_prob directly (FG-53). Add infallible
constructors Normal::standard/Uniform::unit/Beta::uniform_prior/
Bernoulli::fair (FG-29). Add interior-point known-answer tests for every
distribution and exact-value tests for log_sum_exp/normalize_log_probs/
log1p_exp (FG-06, FG-32), plus regression tests at every previously-guarded
point.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2
…guides, ELBO honesty (FG-04, FG-16, FG-17, FG-18, FG-44, FG-45, FG-46, FG-60)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2
…address collisions, dup detection, fresh score logp, i64 path, pattern binds (FG-19, FG-20, FG-21, FG-26, FG-47, FG-48, FG-52, FG-54, FG-61)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2
…ence and correct ABC-SMC (FG-03, FG-09, FG-13, FG-34, FG-43, FG-56, FG-58, FG-59)

- FG-03: smc_prior_particles weight = log_likelihood + log_factors (prior cancels), no prior-squaring.
- FG-59: move the prior trace into the particle instead of cloning it.
- FG-43/FG-58: adaptive_smc is now genuine likelihood-tempered SMC (adaptive beta ladder, ESS-triggered systematic resampling, pi_beta-invariant MH rejuvenation) returning SMCResult with an unbiased log-evidence estimate.
- FG-13: rejuvenation is a separate pi_beta-invariant move that never reweights; post-move ESS stays == N.
- FG-09: abc_smc is a correct importance-weighted ABC-SMC (Beaumont/Toni) with weighted-covariance perturbation kernel, prior-zero rejection, and w ~ pi/sum_j w_j K; abc_smc_weighted exposes the weighted population.
- FG-34: bounded per-stage attempts with typed ABCError (EmptyInitialPopulation / StageExhausted); no panic, no infinite loop.
- FG-56: is_multiple_of(2) already in place from the trunk migration.

Regression tests: tests/f_smc_smc.rs, tests/f_smc_abc.rs (seeded, analytic references).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2
…n ESS, split-R-hat, autocorr Geweke, and redundant-work fixes (FG-01, FG-02, FG-10, FG-11, FG-12, FG-35, FG-36, FG-37, FG-38, FG-39, FG-40, FG-41, FG-42, FG-57)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2
…e benches (FG-05, FG-22, FG-24, FG-62, FG-63, FG-64)

FG-05: back Address with Arc<str> + precomputed u64 hash (allocation-free
clone, O(1) hash). Measured adaptive_mcmc_chain before/after on 20/50-site
models: ~1.3% faster at 50 sites (non-overlapping CIs), neutral at 20. The
win is from Arc<str> O(1) clone; the cached hash aids HashMap<Address> probe
sites (MH kind_cache, adaptation, VI params). Honest note: audit's "address
allocation dominates" premise not borne out (trace is BTreeMap/Ord-keyed).

FG-22/FG-62: wire-or-cut decided by evidence. f_perf pooling_evidence showed
PooledPriorHandler/TracePool only ~3.8% faster than the shipped PriorHandler,
below the 10% bar, so CowTrace/TracePool/TraceBuilder/PooledPriorHandler and
the lying memory.md module doc were deleted. All in-crate call sites, tests,
examples, and rustdoc updated.

FG-63/FG-64: TraceBuilder::with_capacity (ignored its argument) resolved
fixed-by-removal with the rest of the subsystem.

FG-24: benches/f_perf.rs benchmarks the real entry points end-to-end
(adaptive_mcmc_chain 20/50-site, tempered adaptive_smc, elbo_with_guide) with
committed baseline numbers; deleted memory_benchmarks.rs (benched deleted code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2
…path (FG-31)

Close the FG-31 competitive-ceiling gap:
- New src/inference/hmc.rs: Hamiltonian Monte Carlo over the f64 sites of a
  trace using deterministic central-finite-difference forces. Leapfrog
  integrator, Hoffman & Gelman (2014) dual-averaging step size to 0.8 target
  (frozen after warmup), Alg.4 reasonable-epsilon init, configurable path
  length L, identity mass with optional diagonal mass adaptation. Bounded
  supports handled by -inf/divergence rejection. Module docs prove exactness:
  finite-difference forces still yield a volume-preserving, reversible leapfrog
  map, so the MH accept step (using the exact Hamiltonian) is exactly correct.
- 7 new distributions in core/distribution.rs following existing style:
  StudentT, Cauchy, Laplace, Weibull, ChiSquared, InverseGamma (f64) and
  DiscreteUniform (i64), each with validated constructors, full normalizing
  constants in log_prob, and rand_distr / inverse-CDF / reciprocal-Gamma
  samplers.
- DiscreteUniform makes ChoiceValue::I64 live end-to-end through
  sample/observe/replay/score and MCMC.
- Exports wired through lib.rs (hmc_chain, HMCConfig, sample_i64, all 7 dists).

Tests (all seeded): interior-point log_prob vs scipy-equivalent closed forms,
boundary/support, moment sanity; HMC on 2-D correlated Gaussian (mean within
3 SE, cov within 15%), conjugate Normal-Normal vs analytic, and >=2x ESS per
model-evaluation vs adaptive_single_site_mh (measured 5.0x); i64 end-to-end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2
…y pruning, exact distribution count, verified MSRV (FG-23, FG-25, FG-33, FG-50, FG-51)

- FG-23: drop the "production-ready" tagline (README, mdBook home, and a
  stale duplicate landing page) for accurate positioning; add an explicit
  pre-1.0 SemVer policy note.
- FG-25: add examples/{smc,abc,vi}_inference.rs -- the first examples/docs
  anywhere in the crate to exercise adaptive_smc, abc_smc_weighted, and
  optimize_meanfield_vi_with_config, each checked against a closed-form
  posterior; wire into a new mdBook "Advanced Inference" tutorial section;
  add hmc_chain to the README's example index.
- FG-33: prune 11 of 22 ErrorCode variants (and the FugueError
  variants/constructors/macro that existed only to hold them) that no code
  path ever constructed; document the live 11 with their construction sites.
- FG-50: state the exact distribution count (17) instead of "10+".
- FG-51: the README's unverified "1.70+" MSRV claim was wrong -- real
  rustc 1.70.0 fails to build the crate. Verified the true floor (1.87.0),
  pinned rust-version in Cargo.toml, corrected the badges, and added an MSRV
  CI job that actually builds against that toolchain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2
…arness, tighten e2e tolerances (FG-14, FG-15, FG-49)

- FG-14: add tests/f_tests_sampler_validation.rs, a KS (continuous, n=5000,
  alpha=0.001, one-sample against hand-derived analytic CDFs) / chi-square
  (discrete) + standardized-moment goodness-of-fit suite covering all 17
  exported distributions, not just Normal. Confirmed the Gamma test catches
  a rate-vs-scale parameterization bug via a manual injection/revert check.
- FG-15: extend src/inference/validation.rs with
  test_conjugate_beta_bernoulli_model/ConjugateBetaBernoulliConfig
  alongside test_conjugate_normal_model, factor shared scoring logic into
  validate_against_analytical_posterior, re-export the new items from
  lib.rs, and add tests/analytical_validation.rs which actually calls both
  harnesses against adaptive_mcmc_chain. Fixed the stale
  inference_integration.rs comment claiming ConjugateNormalConfig "isn't
  exported".
- FG-49: replace the self-acknowledged loose/apologetic tolerances in
  tests/end_to_end_workflows.rs (mse<200, pred_range<100) and
  inference_integration.rs's test_workflow_parameter_estimation_uncertainty
  (beta within 1.5, alpha within 2.0) with CLT-derived bounds computed from
  each model's exact closed-form Bayesian-linear-regression posterior
  (cross-checked in tests/gen_refs.py) and each chain's measured effective
  sample size. Diagnosed that the cross-validation test's original 30
  samples/10 warmup never reached the asymptotic regime (empirically mse
  ~27 vs a theoretical ~0.04-0.6); bumped to 150/50, still sub-second, which
  resolves it without loosening the new bound.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2
Address became a struct with private fields plus as_str() (FG-05
Arc<str> remediation); the example still used tuple-field .0 access.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2
…mple

FG-47 duplicate-sample detection newly (and correctly) rejects the
hierarchical_models example, which sampled each group's intercept/slope
inside the per-observation plate — drawing the same alpha#g/beta#g/class#c
address once per observation in that group. Before FG-47 this silently
double-counted the group log-prior and dropped all but the last choice.

Restructure every affected model to sample each group parameter once in a
group-level plate, then index it per observation. This is both the correct
partial-pooling model and conflict-free.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2
…(FG-55)

Implement the public `Validate` trait for the ten exported distributions
that lacked an impl (LogNormal, Binomial, Poisson, StudentT, Cauchy,
Laplace, Weibull, ChiSquared, InverseGamma, DiscreteUniform), bringing
coverage to all 17 distributions re-exported at the crate root. Each impl
mirrors its `new()` constructor's validation exactly (predicates,
messages, error codes, context keys).

Add coverage tests:
- tests/f_validate_coverage.rs: public-API exhaustiveness/drift guard that
  calls `.validate()` on a valid instance of every exported distribution
  (all 17 enumerated); fails to compile if a distribution is exported
  without a Validate impl.
- src/core/distribution.rs: fg55_validate_rejects_invalid_parameters, one
  invalid-parameter case per newly implemented distribution asserting the
  correct ErrorCode (uses struct literals for the otherwise-unreachable
  invalid instances).
- src/error.rs: extend validate_trait_on_valid_distributions to all 17.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2
FG-10: rewrite the categorical regression to K=8 (and K=12) targets whose
posterior mass sits on high indices, with per-category + aggregate-L1 tolerances
that fail on the pre-fix asymmetric UniformCategoricalProposal and pass on the
prior-resample fix.

FG-19: fold sequence_vec/traverse_vec (hence plate!) into a right-nested,
Vec-threaded bind chain so interpretation stays O(1) stack for any association;
add a 100k-site plate! stack-safety regression (left-fold shape) plus an ordering
test.

FG-20/FG-21: make single-site MH sample fresh dimensions from the prior as an
RJMCMC birth (with the matching q_forward/q_reverse prior terms) and add the
site-selection dimension term, so structure-varying models recover the correct
trans-dimensional posterior instead of biasing or panicking; refresh the cached
site list on any structural change. Add sampler-path regressions: a b~Bernoulli
branch model that recovers P(b=1|y) and E[x|b=1], and a branch-address-switching
model driven through adaptive_mcmc_chain/adaptive_single_site_mh.

FG-22: scrub the remaining fugue::runtime::memory references from how-to/README,
production-deployment, trace-manipulation, and .github/CHANGELOG; add a docs guard
test so a reintroduced reference fails CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2
…, truthful perf claims (re-verification lows)

ITEM 1 (src/core/distribution.rs) — DiscreteUniform::len() truncation:
The full i64 domain has 2^64 support points; `(high - low + 1) as u64`
truncated that to 0, so sample() panicked on gen_range(0..0) and log_prob()
returned +INF for in-range x. Keep the count in u128 (new private count()),
add is_full_i64_range(), sample the full domain via a raw uniform i64 draw,
and score it as -64*ln2. len() now saturates to u64::MAX for the full range
(documented); every other range is unchanged. Regressions: full-range
sample()/log_prob() (no panic, -64*ln2 within 1e-12) and near-full ranges
(MIN..=MAX-1, MIN+1..=MAX) scoring -ln(2^64 - 1).

ITEM 2 (src/inference/mh.rs) — site-list cache invalidation on equal-count
set swaps: the length-based check the finding describes was already replaced
(FG-20/21) by a born/died `structure_changed` flag that fires on ANY
address-set change, including swaps that keep the site count constant; both
the warmup and sampling loops honor it and the cache-site comment documents
it. Added a regression that drives the exact equal-count switching model
(bool `sw` selecting sample("a") vs sample("b")) and asserts both branches
are visited, both value marginals move (variance > 0), no ghost choices, and
finite weights.

ITEM 3 (docs) — stale claims for the deleted memory subsystem: README.md
and docs/src/home.md still advertised "memory optimization throughout" and
"memory pooling, copy-on-write traces"; the how-to index still labeled the
performance guide "Memory pooling". Replaced with truthful claims
(numerically stable log-space computations; O(1), allocation-free Arc<str>
address clones with a cached hash — what FG-05 shipped). The remaining
"Memory Optimization" mentions in the trace-manipulation tutorial describe
generic techniques (fresh traces, pre-sizing, batching) with no
deleted-subsystem references and were left as-is.

Also wrapped two pre-existing over-width assert! lines in
tests/f_mcmc_proposals.rs so `cargo fmt --all -- --check` passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2
…iation complete)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2
Copilot AI review requested due to automatic review settings July 11, 2026 22:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5821d267c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/inference/hmc.rs
Comment on lines +317 to +320
qq[i] = orig + h;
let lp = log_joint_at(model_fn, base, sites, &qq);
qq[i] = orig - h;
let lm = log_joint_at(model_fn, base, sites, &qq);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid absorbing HMC states near bounded supports

For bounded or positive f64 sites, a current value within finite_diff_eps of the boundary (for example a Gamma, LogNormal, Uniform, or Beta draw with q < 1e-5, or a Beta draw near 1) makes the q - h probe score -inf. That marks the very first gradient as divergent, so every HMC transition from that valid state rejects before moving; chains initialized in this boundary layer get stuck rather than merely having lower acceptance. Consider a support-aware one-sided/adaptive finite difference or transforming constrained sites before applying HMC.

Useful? React with 👍 / 👎.

Comment thread src/inference/mh.rs
Comment on lines +566 to +567
let delta = (self.scale * gaussian_z(self.rng)).round() as i64;
current + delta

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prevent overflow in i64 random-walk proposals

When an i64 latent can sit near i64::MAX or i64::MIN (for example DiscreteUniform over an extreme range), adding the rounded Gaussian step can overflow in debug builds or wrap in optimized builds. That either panics during MCMC or jumps to a value far across the support while the code still treats the proposal as symmetric with no Hastings correction. Compute the proposal in a wider type or use checked/saturating arithmetic with an explicit reject/reflect path.

Useful? React with 👍 / 👎.

@alexnodeland
alexnodeland merged commit a1e49ad into main Jul 13, 2026
5 checks passed
@alexnodeland
alexnodeland deleted the audit/2026-07-remediation branch July 13, 2026 13:18
alexnodeland added a commit that referenced this pull request Jul 13, 2026
…ssion test (#37)

Rebased onto main after the July 2026 audit remediation (#36), which
independently fixed the same stack overflow via an iterative trampoline
in the interpreter (FG-19) and a stack-safe sequence_vec. This keeps the
still-novel parts of the original PR: the criterion benchmark for large
observe sequences, and a 100k-observe regression test exercising the
sequence_vec + observe path (the existing FG-19 test covers sample+bind).


Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2

Co-authored-by: Brendan Ashworth <brendan.ashworth@me.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants