Skip to content

Commit de16cda

Browse files
alexnodelandclaude
andauthored
audit: remediate all 106 findings from the July 2026 ecosystem audit (#9)
* docs: add July 2026 audit findings document (EV-01..EV-106) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * audit(e-pop): fix EV-07, EV-08, EV-13, EV-28, EV-84, EV-85 EV-07: Individual::set_fitness now panics on NaN (documented invariant); best()/worst()/sort_by_fitness() are NaN-safe via a total order that ranks NaN strictly worst. EV-08: best()/worst()/sort_by_fitness() delegate to FitnessValue::is_better_than() (new cmp_by_quality helper) instead of a to_f64() scalar, fixing wrong results for ParetoFitness with infinite crowding distances. EV-13: NSGA-II crowding distance for parent selection is recomputed per non-dominated front (Deb 2002) via recompute_crowding_distance_per_front, replacing the whole-population computation in step/step_bounded/run/run_bounded. EV-28: genome_mut() and new set_genome() clear the cached fitness/evaluated flag. EV-84: binary tournament draws two distinct competitors (sample without replacement). EV-85: closure MultiObjectiveFitness replaced by ClosureMultiObjective wrapper with an explicit objective count instead of hardcoded 2. Regression tests added for each finding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * audit(e-ops): fix EV-27, EV-71, EV-72, EV-101, EV-102, EV-103, EV-104 EV-27: SubtreeMutation now derives its replacement-subtree depth budget from max_depth - depth(mutation point) and accounts for generate_grow's +1 growth, so the genome max_depth invariant is preserved (with a terminal fallback guard). EV-71: bounded SBX now uses Deb & Agrawal's bounds-aware spread factor (truncated beta_l/beta_u per gene) so children land inside [min,max] by construction with no boundary probability atom, replacing the old clamp-only path. EV-72: split the conflated probability into per-pair crossover_probability (default 0.9, reported by the trait) and per-gene exchange_probability (default 0.5, canonical), with distinct fields, builders, and docs. EV-101: hand-written Default for SwapMutation/PermutationSwapMutation/InsertMutation delegates to new() (count = 1) instead of the derived count = 0 no-op. EV-102: unbounded PolynomialMutation applies a local Gaussian perturbation (sigma default 0.1*(1+|x|), configurable) instead of fabricating +/-1e10 bounds. EV-103: MutationOperator::mutation_probability now returns Option<f64>, reporting None for the length-dependent 1/n default rather than a false 1.0. EV-104: TournamentSelection samples with replacement by default (canonical); without_replacement() retains the distinct-competitor variant; determinism docs fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * audit(e-cmaes): fix EV-01, EV-05, EV-24, EV-36, EV-37, EV-38, EV-40, EV-62, EV-97 CMA-ES (cmaes.rs): - EV-01: replace the broken hand-rolled cyclic-Jacobi routine (which never mutated its input and returned wrong/negative eigenvalues) with nalgebra::SymmetricEigen. Add eigenpair/positivity/reconstruction tests plus a seeded 5-D Rosenbrock convergence test. - EV-36: evaluate the bound-repaired (feasible) point but adapt the distribution (m/C/paths/sigma) from the UNREPAIRED sample; add an optional quadratic boundary penalty. Best-solution tracking moved to step() and reports the feasible point. - EV-37: recompute the eigensystem on the correct per-generation cadence max(1, floor(1/(10 n (c1+cmu)))) instead of a factor-of-lambda too infrequent. - EV-38: replace the trace-only eigendecomposition test with real eigenvalue/eigenvector/reconstruction assertions. Self-adaptive ES (self_adaptive.rs): - EV-05/EV-24: swap the log-normal learning rates so the shared once-per- individual deviate carries tau'=1/sqrt(2n) and per-coordinate deviates carry tau=1/sqrt(2 sqrt n) (Beyer & Schwefel); fix LearningRates::for_dimension and docs. - EV-97: isotropic case uses tau0 = 1/sqrt(n). - EV-62: rename the 1e-10 constant to SIGMA_UNDERFLOW_FLOOR (honest doc) and add a configurable, problem-scaled min_sigma (default 1e-8*initial_sigma). Evolution Strategy (evolution_strategy.rs): - EV-40: default self-adaptive ES now uses (mu,lambda) comma selection; plus remains an explicit option; docs explain why elitism suppresses sigma self-adaptation. Thread the configurable min_sigma through the mutation path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * audit(e-hyper): EV-21 EV-22 EV-23 EV-35 EV-61 EV-86 EV-95 EV-96 Redesign the operator-parameter learner as an honest Thompson-sampling bandit and wire it into SimpleGA; fix the Gamma/Beta/log-moment estimators; add turbofish-free builder entry points and build-time config validation. - EV-23/EV-21: ThompsonSamplingTuner — each tunable parameter is discretized into arms, each arm holds a Beta posterior over P(improve | arm); Thompson arm-selection applies the arm VALUE (never the Beta draw). Wired into SimpleGA via .adaptive_operators(ThompsonConfig) + run_adaptive(); example and integration test exercise the real loop and assert feedback flows. - EV-22: GammaPosterior is an honest rate posterior; adds posterior_mean_of_mean() = beta/(alpha-1); eta params no longer shoehorned into an exponential likelihood. - EV-61: LogNormalPosterior -> RunningLogMoments; Welford variance with no small-n prior contamination. - EV-95: BetaPosterior::observations() = (alpha-alpha0)+(beta-beta0). - EV-96: PolynomialDecay doc formula corrected to match the implementation. - EV-35: SimpleGABuilder::real_valued()/bit_string()/permutation() — zero turbofish; sphere_optimization example updated + negation comment fixed. - EV-86: build() validates population/elite/crossover-prob/bounds with typed Configuration errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * audit(e-interactive): fix EV-06, EV-25, EV-26, EV-63, EV-64, EV-65, EV-66, EV-67, EV-68, EV-69, EV-70, EV-98, EV-99, EV-100 Bradley-Terry (bradley_terry.rs): - EV-06: recompute_all() is now driven from process_pairwise so the live loop re-fits the MLE. - EV-67: genuine Gaussian log-strength prior (NR) + Gamma pseudo-count prior (MM) keep all-win/all-loss finite. - EV-25/EV-66: strength-scale covariance via delta method from the sum-to-zero-constrained Fisher pseudo-inverse (ridge hack dropped); NR and MM now report Var(pi) on the same scale. - EV-65: Armijo sufficient-INCREASE condition (sign fixed) with a testable backtracking line search. Aggregation (aggregation.rs): - EV-06: process_pairwise re-fits Bradley-Terry before returning fitness. - EV-98: ImplicitRanking variance propagated to the score scale; Elo variance given a positive steady-state floor. Selection (selection_strategy.rs): - EV-68: find_informative_pair excludes the already-chosen index (no self-pairs). - EV-69: exploration/coverage bonuses normalized by mean population variance (model-agnostic). - EV-70: zero-variance/known pairs score ~0 (not max); only truly unobserved pairs get the max sentinel. - EV-99: entropy consistently in nats; sentinel = ln(2). - EV-100: softmax inverse-CDF fallback picks the last index, not index 0. Session/algorithm (session.rs, algorithm.rs): - EV-26/EV-64: update_fitness* are pure setters; evaluation_count is owned solely by the presented-id loop (exactly once per response, symmetric in pairwise). - EV-63: elites keep their CandidateId (and history) across generations. Adds regression tests for every finding (lib + tests/e-interactive_audit.rs). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * audit(e-genome): EV-03, EV-04, EV-19, EV-20, EV-55, EV-56, EV-57, EV-58, EV-59, EV-60, EV-91, EV-92, EV-93, EV-94 Genome-layer audit remediation: - EV-03: CompositeGenome trace round-trip now delegates to each component's own to_trace/from_trace, namespacing entries under "first/"/"second/" — no hardcoded encoding prefixes. Works for Permutation/Tree components. - EV-04: TreeGenome trace encode/decode is now lossless (function -> index in the stable F::functions(); terminal -> (discriminant, payload) via new Terminal::encode/decode). from_trace(to_trace(g)) reproduces g exactly. - EV-19/EV-20/EV-55: distance() panics on structural mismatch (was silent 0.0 / truncation); added try_distance() -> Result on the trait and per type. - EV-56: added Bounds::try_new (rejects min > max); normalize()/denormalize() handle degenerate min==max (0.5 / min) instead of dividing by zero. - EV-57: added length-aware variation operators for DynamicRealVector in src/genome/dynamic_ops.rs (cut_and_splice + DynamicGaussianMutation). - EV-58: DynamicRealVector::generate no longer panics on empty bounds; added try_generate() -> Result for the degenerate case. - EV-59: RealVector/BitString/Permutation from_trace distinguish MissingAddress (stop) from a present-but-wrong-typed choice (TypeMismatch error). - EV-60: iterative eval/depth/size + explicit iterative teardown for deep trees (a Drop impl is impossible — E0509 in the un-owned operator layer). - EV-91: DynamicRealVector to_trace/from_trace derive the gene address from trace_prefix() so it cannot diverge. - EV-92: renamed new_unchecked -> from_vec_unchecked with documented invariants and a debug_assert!(is_valid). - EV-93: EvolutionaryGenome::distance is now a required trait method. - EV-94: documented the MultiBounds-as-length convention on generate() and added honest per-type constructors (generate_with_len / generate_with_depth). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * audit(e-integration): EV-16, EV-17, EV-18, EV-51, EV-52, EV-53, EV-54, EV-90 Make the fugue_integration layer a genuine "evolution as Bayesian inference" pipeline over the Boltzmann posterior pi_beta(x) ∝ p(x)·exp(beta·f(x)). - EV-16: EvolutionarySMC is now a valid tempered SMC sampler — incremental importance weights w_t = exp((beta_t - beta_{t-1})·f(x)) from the pre-move state, accumulated across non-resampling steps, self-normalised, ESS-triggered systematic resampling with weights reset to uniform, and pi_beta-invariant MH mutation/crossover rejuvenation. Regression test matches the analytic Gaussian conjugate posterior. - EV-52: to_weighted_trace runs a real fugue `factor(beta·f)` model through the new TraceScoringHandler so total_log_weight() == beta·f(x) (log_factors), not 0. - EV-51: effect handlers rewritten on fugue's real Handler contract (TraceScoringHandler, RecordingHandler with correct log-weight bookkeeping); the ad-hoc "Poutine handlers" are honestly renamed to operation hooks and the stale/zero per-choice logp is no longer fabricated. - EV-90: EvolutionStep MH acceptance uses the full target incl. the prior, so out-of-bounds proposals are rejected; stationary law is the on-bounds Boltzmann posterior (test: no escape + mean matches truncated-exponential). - EV-53: HBGA replaced by BayesianAdaptiveGA — conjugate Beta/Gamma posteriors over per-operator success probability + improvement rate, Thompson sampling each generation (renamed honestly: single-level, not "hierarchical"). - EV-54: gaussian_mutation / bounded_mutation use rand_distr::Normal(0, sigma) so the perturbation std is exactly sigma (test on sample std). - EV-17/EV-18: examples/bayesian_evolution.rs runs a genuine end-to-end pipeline through the layer (fugue Model/Handler/factor); module docs state the exact math; integration tests in tests/e_integration_bayesian.rs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * audit(e-algos): EV-10 EV-11 EV-12 EV-14 EV-15 EV-39 EV-41 EV-42 EV-43 EV-44 EV-49 EV-50 EV-73 EV-79 EV-80 EV-81 EV-82 EV-83 EV-88 EV-105 Island model (island.rs): - EV-12 persistent per-island StdRng seeded once from the master RNG -> seeded runs are bit-reproducible even with parallel island evaluation. - EV-11 FullyConnected/Star migration broadcasts to ALL topology targets. - EV-41 immigrants replace the island's worst members, never the best. - EV-82 evaluation counter counts only newly-evaluated individuals. - EV-83 saturating elite arithmetic (no usize underflow for large elitism). Steady-state (steady_state.rs): - EV-43 generate/evaluate exactly offspring_count children. - EV-42 ReplaceRandom/ReplaceWorst/TournamentWorst replace unconditionally; new ReplaceIfBetter is the elitist accept-if-better option. - EV-44 optional prevent_duplicates(bool) genome de-duplication (O(n)). UMDA (eda/umda.rs): - EV-39 rejection sampling from the truncated region (retry 100 then clamp). - EV-81 Bessel (n-1) sample variance. - EV-80 selection uses is_better_than, not to_f64. - EV-79 builder validates ranges in build() (Result) instead of clamping. - EV-10 tests: seeded UMDA beats a same-budget random-search baseline and the learned model converges to the optimum (via new run_with_model). Convergence (diagnostics/convergence.rs): - EV-14 evolutionary_rhat truncates chains to the common minimum length. - EV-49 target-fitness check reads the tracked running best. - EV-50 Stagnation criterion honors its configured threshold. - EV-88 R-hat uses incremental prefix-sum statistics (no full-history rescan). Termination (termination/mod.rs): - EV-73/EV-105 FitnessStagnation uses best-so-far window improvement. Benchmarks (fitness/benchmarks.rs): - EV-15 DixonPrice::optimal_solution() uses the canonical 1-based exponent. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * audit(e-tests): EV-106 seed property tests, add determinism + NaN-policy regression suite - tests/property_tests.rs: replace rand::thread_rng() with StdRng seeded from a proptest-generated any::<u64>() input in all 10 RNG-driven proptests, so failures reproduce byte-for-byte via proptest's own seed/shrink file instead of drawing fresh entropy on every re-run. - tests/e_tests_seeded_determinism.rs (new): for SimpleGA, CMA-ES, (mu+lambda)-ES, UMDA, and NSGA-II, assert two runs with the same StdRng seed produce an identical best genome/fitness (or final population), and two runs with different seeds (almost surely) differ. - tests/e_tests_nan_policy.rs (new): assert Population::evaluate, SimpleGA::run, EvolutionStrategy::run, and ContinuousUMDA::run all panic with a documented message when a fitness function returns NaN, per the EV-07 wave-A policy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * audit(e-checkpoint): EV-02, EV-45, EV-46, EV-47, EV-48, EV-87, EV-89 - EV-02: real RNG round-tripping. Add rand_chacha (serde1) and a SnapshotRng trait (ChaCha8/12/20Rng) with capture()/restore(); Checkpoint::with_rng / restore_rng thread it through. Rewrite examples/checkpointing.rs to prove bit-identical resume (20 straight vs 10 + disk checkpoint + restore + 10); add integration test tests/e-checkpoint_resume.rs. - EV-45: version compatibility now checked uniformly (JSON, binary, compressed); VersionTooOld is actually returned for pre-minimum schemas. - EV-46: CheckpointManager scans existing files on construction and continues the index after the max found (restart-safe). - EV-47: atomic writes - serialize to <path>.tmp, fsync, then fs::rename. - EV-48: configurable bincode size limit (default 256 MiB); oversized files and corrupt length prefixes return a typed TooLarge error. - EV-87: checkpoint indices zero-padded to 8 digits; load_latest orders by parsed numeric index (backward-compatible with legacy names). - EV-89: checkpoint (de)serialization errors preserve the source chain via #[source] (new SerializeError/DeserializeError variants). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * audit(e-wasm): EV-09, EV-32, EV-33, EV-34, EV-75, EV-76, EV-77 - EV-09: install console_error_panic_hook in the wasm_bindgen(start) init so Rust panics log real file/line instead of an opaque `unreachable` trap; document panic semantics. JS-boundary audit: no panicking unwraps remain (fitness helpers use saturating unwrap_or fallbacks). - EV-32: route every fallible EvolutionError boundary through evolution_error_to_js so JS receives structured {type, message} errors; the previously-dead function is now live. - EV-33: report the REAL NSGA-II evaluation count via a counting fitness decorator (CountingMoFitness); delete the fabricated pop*gens*2 formula. - EV-34: add a native incremental stepping API to SimpleGA (init_run/step_generation/finish_run) and refactor run() to drive it (behavior identical); expose a SteppedRealOptimizer in wasm with step(n)/progress getters/early cancel. - EV-75: remove unused serde-wasm-bindgen and wasm-bindgen-futures deps. - EV-76: CMA-ES fitness_history now records per-generation best-so-far by driving step() directly, instead of a single flat point. - EV-77: add an IslandModelOptimizer wasm binding (Ring/FullyConnected/Star topologies, best-k migration) built on the step API plus new migration helpers (SimpleGaRun::best_genomes / SimpleGA::inject_migrants); the native island module is unavailable in wasm (requires the parallel/rayon feature). - Tests: native step/migration equivalence tests; host regression tests (e_wasm_regressions.rs) plus browser wasm_bindgen tests covering the step API, island model, CMA-ES history, real NSGA-II count, and structured errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * audit(e-meta): metadata, license, dependency and claims cleanup (EV-29, EV-30, EV-31, EV-74, EV-78) - EV-29: reconcile license metadata to a single MIT license (matching fugue-ppl) in fugue-evo/Cargo.toml and crates/fugue-evo-wasm/Cargo.toml; add a root LICENSE (MIT text, copyright Alex Nodeland 2025-2026); update both READMEs off the unsupported "MIT OR Apache-2.0" claim (no LICENSE-APACHE/LICENSE-MIT texts ever existed for that claim). - EV-30: verified a `path = "../fugue", version = "0.1.0"` dependency compiles cleanly against the current sibling checkout (cargo check --all-targets, cargo test --lib: 645 passed) -- but the sibling `fugue` repo is independently, concurrently under its own active audit remediation and was observed to go from green to red minutes later from unrelated in-progress edits with zero change on the fugue-evo side. A hard path dependency makes fugue-evo's own build only as stable as fugue's current, possibly-mid-edit working tree, which is exactly the fragility a crate published independently to crates.io should not have. Kept `fugue-ppl = "0.1.0"` pinned to the registry release instead, and documented (with a verified-working example) how to opt into the sibling via a local, uncommitted `[patch.crates-io]` override in README.md's new Development section. - EV-31: move crates/fugue-evo-wasm's [profile.release] (opt-level "s", LTO) to the workspace-root Cargo.toml, where cargo actually honors it; the member manifest keeps a comment pointing at the root. - EV-74: proptest unconditionally pulls in rand 0.9/rand_core 0.9/rand_chacha 0.9 alongside our own rand 0.8 stack; confirmed via `cargo tree -i` that this holds even pinned to proptest 1.7.0 and that the upstream fugue sibling has the identical duplication despite the same proptest = "1.0" pin. Documented as upstream-forced in Cargo.toml rather than "fixed" by a version bump. - EV-78: reworded the misleading "we negate because fugue-evo maximizes" comment in examples/sphere_optimization.rs (Sphere already negates internally) and fixed the printed "Best fitness" to un-negate the sum of squares so it reads as the expected near-zero, non-negative optimum; scanned other examples for the same pattern (none found; the one other "negate" comment, in symbolic_regression.rs, describes a genuine user-authored negation and is accurate as-is). - README/SPEC surgical updates: Bayesian hyperparameter learning is a wired, opt-in ThompsonSamplingTuner; the Fugue integration runs a genuine tempered-SMC/Boltzmann pipeline with a flagship bayesian_evolution.rs example; checkpointing supports bit-identical resume for the ChaCha RNG family. SPEC.md gets a dated implementation-note callout rather than a rewrite. - CHANGELOG.md: added an Unreleased section summarizing the full 2026-07 audit remediation (EV-01 through EV-106). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * audit(fixup): address re-verification findings Fixes the re-verification items flagged after the 2026-07 remediation: - EV-02: add a library-level checkpoint resume path — SimpleGA::checkpoint_run / resume / run_from_checkpoint thread a SnapshotRng (ChaCha) through the run loop for bit-identical resume; un-gate the incremental stepping API for all builds; rewrite examples/checkpointing.rs to use it; add regression tests. - EV-04: add Pow to ArithmeticFunction::functions() (and protect Pow against NaN) so every function variant round-trips losslessly; harden encode_function with a debug_assert; add per-variant and Pow round-trip regression tests. - EV-17: correct the crate-level framing in lib.rs/README.md/SPEC.md so the "evolution as Bayesian inference" headline scopes to fugue_integration and the default algorithms are described as standalone EC using Trace as a container. - EV-21: document schedules.rs and adaptive.rs as unintegrated building blocks (hyperparameter/mod.rs + module docs), matching the Beta/Gamma treatment. - EV-34: add per-generation progress/cancel callbacks to all remaining WASM optimizers (BitString/Permutation/Nsga2/SymbolicRegression via the step API; ES/UMDA via new native run_with_callback hooks); add tests. - EV-35: replace the 7-param turbofish quickstart with SimpleGABuilder::real_valued() in README, lib.rs, examples, and docs. - EV-60: add a stack-safe Drop for TreeNode (mem::take teardown), make the position collectors and PointMutation iterative; add 100k-deep drop/positions/ mutation regression tests. - EV-74: pin proptest = ">=1.5, <1.7" to collapse the duplicate rand major; add a `make deps-check` (cargo tree -d) CI guard; correct the Cargo.toml/ CHANGELOG notes. - EV-83: add an island regression test for elitism > population size. - REG-1: track a pure running max (separate from the stagnation-throttled best) so target-fitness detection is not missed when stagnation_threshold > tolerance. - REG-2: correct the CHANGELOG EV-30 entry to match the actual crates.io dependency and the README's documented rationale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * audit(fixup): stabilize compute_rhat variance + island seed derivation (re-verification lows) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * style: cargo fmt stragglers from fixup wave Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * audit(e-integration): depend on sibling fugue-ppl via path (EV-30 follow-up) Flip fugue-ppl from the published crates.io release to the co-developed sibling checkout via `path = "../fugue"` (version = "0.1.0" retained), so fugue-evo's Fugue integration is finally exercised against the actual co-developed source now that fugue's 2026-07 audit remediation has landed with a green full-test gate. Adapt genome::composite trace namespacing to fugue's new Address struct (Address::new(..)/addr.as_str() replacing the former tuple-struct Address(..) constructor and .0 field); no behavior change. Reconcile the now-stale EV-30 CHANGELOG entry and README "Development" section, which previously documented the deliberate crates.io pin. Full suite green: 706 unit/integration tests + 3 doctests pass; bayesian_evolution example runs end-to-end; wasm crate cargo check clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * docs: fill all 106 audit resolutions + re-verification verdicts (remediation complete) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * ci: clone sibling fugue for path dependency Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * ci(fixup): satisfy newer-stable clippy and rustdoc lints Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 * test(fixup): bound deep-tree regression test resources (CI OOM) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DEAKcbnLVP8iSXub2Pqor2 --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 3a3b6df commit de16cda

79 files changed

Lines changed: 15092 additions & 2569 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ jobs:
1818
steps:
1919
- uses: actions/checkout@v4
2020

21+
- name: Checkout sibling fugue (path dependency)
22+
run: git clone --depth 1 --branch "${GITHUB_HEAD_REF:-main}" https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue" || git clone --depth 1 --branch main https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue"
23+
2124
- name: Install Rust toolchain
2225
uses: dtolnay/rust-toolchain@stable
2326

@@ -41,6 +44,9 @@ jobs:
4144
steps:
4245
- uses: actions/checkout@v4
4346

47+
- name: Checkout sibling fugue (path dependency)
48+
run: git clone --depth 1 --branch "${GITHUB_HEAD_REF:-main}" https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue" || git clone --depth 1 --branch main https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue"
49+
4450
- name: Install Rust toolchain
4551
uses: dtolnay/rust-toolchain@stable
4652
with:
@@ -55,6 +61,9 @@ jobs:
5561
steps:
5662
- uses: actions/checkout@v4
5763

64+
- name: Checkout sibling fugue (path dependency)
65+
run: git clone --depth 1 --branch "${GITHUB_HEAD_REF:-main}" https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue" || git clone --depth 1 --branch main https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue"
66+
5867
- name: Install Rust toolchain
5968
uses: dtolnay/rust-toolchain@stable
6069
with:
@@ -80,6 +89,9 @@ jobs:
8089
steps:
8190
- uses: actions/checkout@v4
8291

92+
- name: Checkout sibling fugue (path dependency)
93+
run: git clone --depth 1 --branch "${GITHUB_HEAD_REF:-main}" https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue" || git clone --depth 1 --branch main https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue"
94+
8395
- name: Install Rust toolchain
8496
uses: dtolnay/rust-toolchain@stable
8597

@@ -103,6 +115,9 @@ jobs:
103115
steps:
104116
- uses: actions/checkout@v4
105117

118+
- name: Checkout sibling fugue (path dependency)
119+
run: git clone --depth 1 --branch "${GITHUB_HEAD_REF:-main}" https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue" || git clone --depth 1 --branch main https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue"
120+
106121
- name: Install Rust toolchain
107122
uses: dtolnay/rust-toolchain@stable
108123

@@ -128,6 +143,9 @@ jobs:
128143
steps:
129144
- uses: actions/checkout@v4
130145

146+
- name: Checkout sibling fugue (path dependency)
147+
run: git clone --depth 1 --branch "${GITHUB_HEAD_REF:-main}" https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue" || git clone --depth 1 --branch main https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue"
148+
131149
- name: Install Rust toolchain
132150
uses: dtolnay/rust-toolchain@stable
133151

@@ -160,6 +178,9 @@ jobs:
160178
steps:
161179
- uses: actions/checkout@v4
162180

181+
- name: Checkout sibling fugue (path dependency)
182+
run: git clone --depth 1 --branch "${GITHUB_HEAD_REF:-main}" https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue" || git clone --depth 1 --branch main https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue"
183+
163184
- name: Install Rust toolchain
164185
uses: dtolnay/rust-toolchain@stable
165186
with:

.github/workflows/docs.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ jobs:
2828
- name: Checkout
2929
uses: actions/checkout@v4
3030

31+
- name: Checkout sibling fugue (path dependency)
32+
run: git clone --depth 1 --branch "${GITHUB_HEAD_REF:-main}" https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue" || git clone --depth 1 --branch main https://github.com/alexnodeland/fugue.git "$GITHUB_WORKSPACE/../fugue"
33+
3134
- name: Install Rust toolchain
3235
uses: dtolnay/rust-toolchain@nightly
3336

AUDIT-2026-07.md

Lines changed: 1693 additions & 0 deletions
Large diffs are not rendered by default.

CHANGELOG.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,61 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
Remediation of the full 2026-07 audit (`AUDIT-2026-07.md`, findings EV-01
11+
through EV-106: correctness, math, completeness, usability, elegance, and
12+
docs issues across CMA-ES, hyperparameter learning, interactive/Bradley-Terry
13+
ranking, genome traces, population/operators, algorithms, Fugue integration,
14+
checkpointing, the WASM bindings, and package metadata/dependencies).
15+
16+
### Fixed
17+
18+
- `Individual::set_fitness` now panics on a NaN fitness value, and `Population::best`/`worst`/`sort_by_fitness` treat NaN as strictly worst, so a NaN-fitness individual can no longer be silently reported as the best/worst (EV-07).
19+
- `Population::best`/`worst`/`sort_by_fitness` now rank via `FitnessValue::is_better_than` (new `cmp_by_quality` total order) instead of a `to_f64()` scalar, returning the correct result for `ParetoFitness` with infinite crowding distances (EV-08).
20+
- NSGA-II now recomputes crowding distance per non-dominated front (Deb 2002) rather than over the whole mixed-rank population, correcting binary-tournament parent-selection diversity pressure and the reported `crowding_distance` (EV-13).
21+
- `Individual::genome_mut` now clears the cached fitness, and a new `Individual::set_genome` does the same, so a mutated genome is always re-evaluated (EV-28).
22+
- NSGA-II binary tournament now draws two distinct competitors (sampling without replacement) (EV-84).
23+
- SubtreeMutation (GP) no longer violates a genome's max_depth; the replacement subtree is generated within the depth budget max_depth - depth(mutation point), preventing bloat-control overruns (EV-27).
24+
- Bounded Simulated Binary Crossover now uses Deb & Agrawal's bounds-aware spread factor so offspring fall inside [min,max] by construction, eliminating the probability mass previously piled onto the bounds by clamping (EV-71).
25+
- SwapMutation, PermutationSwapMutation, and InsertMutation `Default::default()` now perform one operation instead of being a silent no-op (EV-101).
26+
- Composite genome trace round-trip now delegates to each component's own to_trace/from_trace under a 'first/'/'second/' namespace, fixing silent data loss for Permutation and Tree components (EV-03).
27+
- TreeGenome trace encode/decode is now lossless: function nodes serialize as their index in the stable ArithmeticFunction::functions() ordering and terminals as a (discriminant, payload) pair, so from_trace(to_trace(g)) reproduces g exactly (EV-04).
28+
- DynamicRealVector::generate no longer panics on empty bounds; added try_generate -> Result for the degenerate case (EV-58).
29+
- from_trace on RealVector/BitString/Permutation now returns GenomeError::TypeMismatch for a present-but-wrong-typed choice instead of silently truncating, distinguishing it from a genuinely missing address (EV-59).
30+
- Deep GP trees: eval/depth/size are now iterative (explicit stack) and an iterative teardown (TreeGenome::dismantle / drop_node_iteratively) is provided so pathologically deep trees no longer overflow the stack (EV-60).
31+
- DynamicRealVector trace I/O now derives its gene address from trace_prefix(), so the advertised and actual prefixes can no longer diverge (EV-91).
32+
- Interactive/Bradley-Terry: the MLE is now re-fit inside the live pairwise loop (via process_pairwise), so pairwise user feedback actually orders candidates (EV-06).
33+
- Interactive/Bradley-Terry: Newton-Raphson uses a Gaussian log-strength prior and MM a Gamma pseudo-count prior, keeping all-win/all-loss candidates finite; the `regularization` field is renamed `prior_lambda` (default 0.1, serde alias retained) (EV-67).
34+
- Interactive/Bradley-Terry: uncertainty is reported on the strength scale for both optimizers via a delta-method, sum-to-zero-constrained Fisher pseudo-inverse (the ridge-inflation bug is gone) (EV-25, EV-66).
35+
- Interactive/Bradley-Terry: the backtracking line search now enforces the correct Armijo sufficient-increase condition (EV-65).
36+
- Interactive/Aggregation: Elo and ImplicitRanking uncertainties are now on the same scale as their means (Elo gains a steady-state floor; ImplicitRanking uses the score-scale binomial variance) (EV-98).
37+
- Interactive/Selection: CoverageAware pairing never returns a self-pair; exploration/coverage bonuses are normalized to a model-agnostic scale by mean population variance, and zero-variance (already-known) pairs score ~0 instead of the max-uncertainty sentinel (EV-68, EV-69, EV-70).
38+
- License metadata reconciled: `fugue-evo` and `fugue-evo-wasm` now declare a single `license = "MIT"` (matching `fugue-ppl`'s `license = "MIT"`), and a root `LICENSE` file (MIT text, copyright Alex Nodeland 2025-2026) is now shipped; README/crate docs no longer claim a dual MIT-OR-Apache-2.0 license with no accompanying license texts (EV-29).
39+
- `fugue-ppl` now resolves to the co-developed sibling crate via `fugue-ppl = { path = "../fugue", version = "0.1.0" }` instead of the published `fugue-ppl = "0.1.0"` crates.io release, so `fugue-evo`'s Fugue integration is finally built and tested against the actual co-developed `../fugue` source rather than a registry release the two crates were never exercised against together — the gap this finding was originally about. This became safe once `fugue`'s own 2026-07 audit remediation landed with a green full-test gate; adapting to that post-remediation API required migrating `genome::composite`'s trace namespacing to the new `Address` struct (`Address::new(..)`/`addr.as_str()` in place of the former tuple-struct `Address(..)` constructor and `.0` field), with no behavior change. The `version = "0.1.0"` field is retained so the dependency still resolves from crates.io if the sibling checkout is absent, and the README "Development" section documents how to pin back to the published release (EV-30).
40+
- `crates/fugue-evo-wasm/Cargo.toml`'s `[profile.release]` (opt-level "s", LTO) has moved to the workspace-root `Cargo.toml`, where Cargo actually honors it; the member manifest previously declared it in a location Cargo silently ignores, leaving the WASM release build unshrunk and non-LTO (EV-31).
41+
- Checkpoint resume is now a first-class library API (EV-02): `SimpleGA::checkpoint_run` snapshots an in-progress incremental run — population, best, evaluations, statistics, and a captured `SnapshotRng` (ChaCha family) — into a `Checkpoint`, and `SimpleGA::resume`/`SimpleGA::run_from_checkpoint` restore it (RNG included) so a resumed run is bit-identical to an uninterrupted one, instead of forcing users to re-implement the generation loop. `resume` rejects a checkpoint with no captured RNG rather than silently diverging. The incremental stepping API (`SimpleGaRun` + `init_run`/`step_generation`/`finish_run`) is now available in all builds (previously `parallel`-gated), and `examples/checkpointing.rs` was rewritten to drive the resume purely through this API.
42+
- Every remaining WASM optimizer now exposes a per-generation progress/cancel callback (EV-34), extending the incremental support beyond the RealVector `SteppedRealOptimizer`: `BitStringOptimizer`, `PermutationOptimizer`, `Nsga2Optimizer`, and `SymbolicRegressionOptimizer` gain `optimizeWithProgress`/`optimizeCustomWithProgress` methods (driven through `SimpleGA::init_run`/`step_generation` and `Nsga2::step`), and `EvolutionStrategyOptimizer`/`UmdaOptimizer` gain `optimizeWithProgress` backed by new native `EvolutionStrategy::run_with_callback` and `UMDA::run_with_callback` hooks. The callback receives `(generation, bestFitness)` (NSGA-II reports the Pareto-front size) and returning `false` cancels the run, so a Web Worker can `postMessage` progress or honor a cancel button instead of blocking on one opaque call.
43+
- Reworded the misleading "we negate because fugue-evo maximizes" comment in `examples/sphere_optimization.rs` (the built-in `Sphere` fitness already negates internally; no user negation is needed) and fixed the printed "Best fitness" to report the un-negated sum-of-squares objective so it reads as the expected near-zero, non-negative value at the optimum (EV-78).
44+
45+
### Changed
46+
47+
- The closure `MultiObjectiveFitness` blanket impl (hardcoded 2 objectives) is replaced by `ClosureMultiObjective::new(num_objectives, closure)`, which reports the true objective count (EV-85).
48+
- SbxCrossover now exposes two separate probabilities — per-pair `crossover_probability` (default 0.9) and per-gene `exchange_probability` (default 0.5, canonical) — via distinct fields and builders (`with_probability`, `with_exchange_probability`) (EV-72).
49+
- Unbounded PolynomialMutation now applies a local Gaussian perturbation (sigma default 0.1*(1+|x|), configurable via `with_unbounded_sigma`) instead of fabricating +/-1e10 bounds (EV-102).
50+
- MutationOperator::mutation_probability now returns Option<f64>, reporting None for the length-dependent 1/n default instead of an untruthful 1.0 (EV-103) **(breaking)**.
51+
- TournamentSelection samples with replacement by default (canonical selection pressure; no longer deterministic when tournament_size >= population size); use `TournamentSelection::without_replacement` for the distinct-competitor variant (EV-104).
52+
- Added length-aware variation operators for DynamicRealVector (cut_and_splice crossover and DynamicGaussianMutation) in the new genome::dynamic_ops module (EV-57).
53+
- Documented the MultiBounds-as-length/depth convention on EvolutionaryGenome::generate and added honest per-type constructors: BitString/Permutation/DynamicRealVector::generate_with_len and TreeGenome::generate_with_depth (EV-94).
54+
- Bounds gained a fallible try_new constructor (rejects min > max); normalize()/denormalize() now handle degenerate min==max bounds (0.5 / min) instead of producing NaN via divide-by-zero (EV-56).
55+
- README/SPEC updated to precisely describe post-remediation behavior: the Bayesian hyperparameter learner is a wired, opt-in `ThompsonSamplingTuner` (`SimpleGABuilder::adaptive_operators` + `run_adaptive`); the Fugue integration runs a genuine tempered-SMC/Boltzmann pipeline with a flagship `examples/bayesian_evolution.rs`; and checkpointing supports bit-identical resume for the ChaCha RNG family (EV-29 through EV-78 doc sweep).
56+
- The duplicate `rand` major in dev/test builds is eliminated by pinning `proptest = ">=1.5, <1.7"`. proptest migrated its internal RNG stack to rand 0.9 in 1.7.0; the 1.5.x/1.6.x line still uses rand 0.8, so pinning below 1.7 collapses the graph back to a single rand major (0.8.5). Verified empirically (`cargo update -p proptest --precise 1.6.0` drops rand 0.9.2/rand_chacha 0.9.0/rand_core 0.9.3, after which `cargo tree -d` shows one rand major and `cargo check --all-targets` + the property-test suite pass). A `make deps-check` target (`cargo tree -d` guard, wired into `make ci`) now fails the build if a duplicate rand major reappears (EV-74).
57+
58+
### Breaking
59+
60+
- `EvolutionaryGenome::distance` is now a required method (no silent 0.0 default) and panics on structural mismatch; a new required `try_distance -> Result` provides the fallible path. RealVector/BitString/Permutation distance no longer silently truncate or report 0.0 on length mismatch (EV-19, EV-20, EV-55, EV-93).
61+
- `Permutation::new_unchecked` renamed to `from_vec_unchecked`, with documented invariants and a debug-build validity assertion (EV-92).
62+
863
## [0.1.0] - 2025-12-12
964

1065
### Added

Cargo.lock

Lines changed: 34 additions & 58 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)