From b4d2b7343f169b7b2fce9964ae0ca75e88e87239 Mon Sep 17 00:00:00 2001 From: rUv Date: Fri, 27 Feb 2026 16:12:45 +0000 Subject: [PATCH] fix: resolve P0 safety issues in ruvector-dither, thermorust, and exo-ai MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace debug_assert with assert for bits bounds in quantize functions - Guard ChannelDither against 0 channels and invalid bits - Handle non-finite beta/rate in Langevin/Poisson noise (return 0) - Remove unused itertools dependency from thermorust - Fix partial_cmp().unwrap() NaN panics across 7 exo-ai files - Fix SystemTime unwrap() in transfer_crdt (use unwrap_or_default) - Fix domain ID mismatch (exo_retrieval → exo-retrieval) in orchestrator - Update tests to match corrected domain IDs Co-Authored-By: claude-flow --- crates/ruvector-dither/src/channel.rs | 2 ++ crates/ruvector-dither/src/quantize.rs | 5 +++-- crates/thermorust/Cargo.toml | 1 - crates/thermorust/src/noise.rs | 16 +++++++++++++--- crates/thermorust/tests/correctness.rs | 1 - .../src/transfer_orchestrator.rs | 4 ++-- .../tests/transfer_pipeline_test.rs | 8 ++++---- .../crates/exo-core/src/backends/neuromorphic.rs | 2 +- .../crates/exo-core/src/backends/quantum_stub.rs | 4 ++-- .../exo-ai-2025/crates/exo-core/src/genomic.rs | 2 +- .../exo-ai-2025/crates/exo-core/src/learner.rs | 2 +- .../crates/exo-federation/src/transfer_crdt.rs | 2 +- .../crates/exo-hypergraph/src/sparse_tda.rs | 2 +- .../crates/exo-temporal/src/quantum_decay.rs | 2 +- 14 files changed, 32 insertions(+), 21 deletions(-) diff --git a/crates/ruvector-dither/src/channel.rs b/crates/ruvector-dither/src/channel.rs index 1e57336b..66f466a7 100644 --- a/crates/ruvector-dither/src/channel.rs +++ b/crates/ruvector-dither/src/channel.rs @@ -32,6 +32,8 @@ impl ChannelDither { /// If the slice is not a multiple of `n_channels`, the remainder is /// processed using channel 0. pub fn quantize_batch(&mut self, activations: &mut [f32]) { + assert!(!self.channels.is_empty(), "ChannelDither must have >= 1 channel"); + assert!(self.bits >= 2 && self.bits <= 31, "bits must be in [2, 31]"); let nc = self.channels.len(); let qmax = ((1u32 << (self.bits - 1)) - 1) as f32; let lsb = 1.0 / qmax; diff --git a/crates/ruvector-dither/src/quantize.rs b/crates/ruvector-dither/src/quantize.rs index 351e9d9b..0ad246dc 100644 --- a/crates/ruvector-dither/src/quantize.rs +++ b/crates/ruvector-dither/src/quantize.rs @@ -21,7 +21,7 @@ use crate::DitherSource; /// ``` #[inline] pub fn quantize_dithered(x: f32, bits: u32, eps: f32, source: &mut impl DitherSource) -> f32 { - debug_assert!(bits >= 1 && bits <= 31, "bits must be in [1, 31]"); + assert!(bits >= 2 && bits <= 31, "bits must be in [2, 31]"); let qmax = ((1u32 << (bits - 1)) - 1) as f32; let lsb = 1.0 / qmax; let dither = source.next(eps * lsb); @@ -50,6 +50,7 @@ pub fn quantize_slice_dithered( eps: f32, source: &mut impl DitherSource, ) { + assert!(bits >= 2 && bits <= 31, "bits must be in [2, 31]"); let qmax = ((1u32 << (bits - 1)) - 1) as f32; let lsb = 1.0 / qmax; for x in xs.iter_mut() { @@ -64,7 +65,7 @@ pub fn quantize_slice_dithered( /// Useful when you need the integer representation rather than a re-scaled float. #[inline] pub fn quantize_to_code(x: f32, bits: u32, eps: f32, source: &mut impl DitherSource) -> i32 { - debug_assert!(bits >= 1 && bits <= 31); + assert!(bits >= 2 && bits <= 31, "bits must be in [2, 31]"); let qmax = ((1u32 << (bits - 1)) - 1) as f32; let lsb = 1.0 / qmax; let dither = source.next(eps * lsb); diff --git a/crates/thermorust/Cargo.toml b/crates/thermorust/Cargo.toml index f7f2bdbd..524e4ade 100644 --- a/crates/thermorust/Cargo.toml +++ b/crates/thermorust/Cargo.toml @@ -15,7 +15,6 @@ readme = "README.md" [dependencies] rand = { version = "0.8", features = ["small_rng"] } rand_distr = "0.4" -itertools = "0.12" [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } diff --git a/crates/thermorust/src/noise.rs b/crates/thermorust/src/noise.rs index a0cc5c9f..4aab9080 100644 --- a/crates/thermorust/src/noise.rs +++ b/crates/thermorust/src/noise.rs @@ -9,16 +9,23 @@ use rand_distr::{Distribution, Normal, Poisson}; /// the noise amplitude must be √(2kT) = √(2/β) in dimensionless units. #[inline] pub fn langevin_noise(beta: f32, rng: &mut impl Rng) -> f32 { + if beta <= 0.0 || !beta.is_finite() { + return 0.0; + } let sigma = (2.0 / beta).sqrt(); Normal::new(0.0_f32, sigma) - .expect("sigma must be finite and positive") + .unwrap_or_else(|_| Normal::new(0.0_f32, 1e-6).unwrap()) .sample(rng) } /// Draw `n` independent Langevin noise samples. pub fn langevin_noise_vec(beta: f32, n: usize, rng: &mut impl Rng) -> Vec { + if beta <= 0.0 || !beta.is_finite() { + return vec![0.0; n]; + } let sigma = (2.0 / beta).sqrt(); - let dist = Normal::new(0.0_f32, sigma).expect("sigma must be finite"); + let dist = Normal::new(0.0_f32, sigma) + .unwrap_or_else(|_| Normal::new(0.0_f32, 1e-6).unwrap()); (0..n).map(|_| dist.sample(rng)).collect() } @@ -27,7 +34,10 @@ pub fn langevin_noise_vec(beta: f32, n: usize, rng: &mut impl Rng) -> Vec { /// Returns the kick to add to a single activation (0.0 if no spike this step). #[inline] pub fn poisson_spike(rate: f64, kick: f32, rng: &mut impl Rng) -> f32 { - let dist = Poisson::new(rate).expect("rate must be > 0"); + if rate <= 0.0 || !rate.is_finite() { + return 0.0; + } + let dist = Poisson::new(rate).unwrap_or_else(|_| Poisson::new(1e-6).unwrap()); let count = dist.sample(rng) as u64; if count > 0 { // Random sign diff --git a/crates/thermorust/tests/correctness.rs b/crates/thermorust/tests/correctness.rs index 6c074ac7..9ff945a5 100644 --- a/crates/thermorust/tests/correctness.rs +++ b/crates/thermorust/tests/correctness.rs @@ -138,7 +138,6 @@ fn cold_system_stays_near_ground_state() { #[test] fn langevin_lowers_energy_on_average() { - use thermorust::energy::SoftSpin; use thermorust::motifs::SoftSpinMotif; let n = 8; let mut motif = SoftSpinMotif::random(n, 1.0, 0.5, 13); diff --git a/examples/exo-ai-2025/crates/exo-backend-classical/src/transfer_orchestrator.rs b/examples/exo-ai-2025/crates/exo-backend-classical/src/transfer_orchestrator.rs index 19af7136..0c364279 100644 --- a/examples/exo-ai-2025/crates/exo-backend-classical/src/transfer_orchestrator.rs +++ b/examples/exo-ai-2025/crates/exo-backend-classical/src/transfer_orchestrator.rs @@ -62,8 +62,8 @@ pub struct ExoTransferOrchestrator { impl ExoTransferOrchestrator { /// Create a new orchestrator. pub fn new(_node_id: impl Into) -> Self { - let src_id = DomainId("exo_retrieval".to_string()); - let dst_id = DomainId("exo_graph".to_string()); + let src_id = DomainId("exo-retrieval".to_string()); + let dst_id = DomainId("exo-graph".to_string()); let mut engine = DomainExpansionEngine::new(); engine.register_domain(Box::new(ExoRetrievalDomain::new())); diff --git a/examples/exo-ai-2025/crates/exo-backend-classical/tests/transfer_pipeline_test.rs b/examples/exo-ai-2025/crates/exo-backend-classical/tests/transfer_pipeline_test.rs index bc8cdf1c..2c4f53f8 100644 --- a/examples/exo-ai-2025/crates/exo-backend-classical/tests/transfer_pipeline_test.rs +++ b/examples/exo-ai-2025/crates/exo-backend-classical/tests/transfer_pipeline_test.rs @@ -60,8 +60,8 @@ fn test_full_transfer_pipeline_multi_cycle() { // - CRDT should know both domain IDs. let prior = orch.best_prior().expect("CRDT must hold a prior"); - assert_eq!(prior.src_domain, "exo_retrieval"); - assert_eq!(prior.dst_domain, "exo_graph"); + assert_eq!(prior.src_domain, "exo-retrieval"); + assert_eq!(prior.dst_domain, "exo-graph"); assert!(prior.improvement >= 0.0 && prior.improvement <= 1.0); assert!(prior.confidence >= 0.0 && prior.confidence <= 1.0); assert!(prior.cycle >= 1); @@ -111,7 +111,7 @@ fn test_crdt_prior_consistency() { } let prior = orch.best_prior().expect("prior must exist after 3 cycles"); - assert_eq!(prior.src_domain, "exo_retrieval"); - assert_eq!(prior.dst_domain, "exo_graph"); + assert_eq!(prior.src_domain, "exo-retrieval"); + assert_eq!(prior.dst_domain, "exo-graph"); assert!(prior.cycle >= 1 && prior.cycle <= 3); } diff --git a/examples/exo-ai-2025/crates/exo-core/src/backends/neuromorphic.rs b/examples/exo-ai-2025/crates/exo-core/src/backends/neuromorphic.rs index 7fd1b638..a2966c97 100644 --- a/examples/exo-ai-2025/crates/exo-core/src/backends/neuromorphic.rs +++ b/examples/exo-ai-2025/crates/exo-core/src/backends/neuromorphic.rs @@ -260,7 +260,7 @@ impl SubstrateBackend for NeuromorphicBackend { SearchResult { id, score, embedding: vec![] } }) .collect(); - results.sort_unstable_by(|a, b| b.score.partial_cmp(&a.score).unwrap()); + results.sort_unstable_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal)); results.truncate(k); let _elapsed = t0.elapsed(); results diff --git a/examples/exo-ai-2025/crates/exo-core/src/backends/quantum_stub.rs b/examples/exo-ai-2025/crates/exo-core/src/backends/quantum_stub.rs index caa5c628..f68bd936 100644 --- a/examples/exo-ai-2025/crates/exo-core/src/backends/quantum_stub.rs +++ b/examples/exo-ai-2025/crates/exo-core/src/backends/quantum_stub.rs @@ -112,7 +112,7 @@ impl InterferenceState { amplitude_im: im, }) .collect(); - measurements.sort_unstable_by(|a, b| b.probability.partial_cmp(&a.probability).unwrap()); + measurements.sort_unstable_by(|a, b| b.probability.partial_cmp(&a.probability).unwrap_or(std::cmp::Ordering::Equal)); measurements.truncate(k); measurements } @@ -180,7 +180,7 @@ impl SubstrateBackend for QuantumStubBackend { SearchResult { id: *id, score: score.max(0.0), embedding: pattern.clone() } }) .collect(); - results.sort_unstable_by(|a, b| b.score.partial_cmp(&a.score).unwrap()); + results.sort_unstable_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal)); results.truncate(k); let _elapsed = t0.elapsed(); results diff --git a/examples/exo-ai-2025/crates/exo-core/src/genomic.rs b/examples/exo-ai-2025/crates/exo-core/src/genomic.rs index 94dfb33c..d66e724e 100644 --- a/examples/exo-ai-2025/crates/exo-core/src/genomic.rs +++ b/examples/exo-ai-2025/crates/exo-core/src/genomic.rs @@ -219,7 +219,7 @@ impl GenomicPatternStore { results.sort_unstable_by(|a, b| { b.weighted_score .partial_cmp(&a.weighted_score) - .unwrap() + .unwrap_or(std::cmp::Ordering::Equal) }); results.truncate(k); results diff --git a/examples/exo-ai-2025/crates/exo-core/src/learner.rs b/examples/exo-ai-2025/crates/exo-core/src/learner.rs index 699c28ec..65bf2390 100644 --- a/examples/exo-ai-2025/crates/exo-core/src/learner.rs +++ b/examples/exo-ai-2025/crates/exo-core/src/learner.rs @@ -149,7 +149,7 @@ impl ReasoningBank { (t, sim) }) .collect(); - scored.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); + scored.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); scored.truncate(k); scored.into_iter().map(|(t, _)| t).collect() } diff --git a/examples/exo-ai-2025/crates/exo-federation/src/transfer_crdt.rs b/examples/exo-ai-2025/crates/exo-federation/src/transfer_crdt.rs index 93b0818d..dfd10fe0 100644 --- a/examples/exo-ai-2025/crates/exo-federation/src/transfer_crdt.rs +++ b/examples/exo-ai-2025/crates/exo-federation/src/transfer_crdt.rs @@ -135,7 +135,7 @@ fn current_millis() -> u64 { use std::time::{SystemTime, UNIX_EPOCH}; SystemTime::now() .duration_since(UNIX_EPOCH) - .unwrap() + .unwrap_or_default() .as_millis() as u64 } diff --git a/examples/exo-ai-2025/crates/exo-hypergraph/src/sparse_tda.rs b/examples/exo-ai-2025/crates/exo-hypergraph/src/sparse_tda.rs index e8272bae..647d6dcc 100644 --- a/examples/exo-ai-2025/crates/exo-hypergraph/src/sparse_tda.rs +++ b/examples/exo-ai-2025/crates/exo-hypergraph/src/sparse_tda.rs @@ -214,7 +214,7 @@ impl SparseRipsComplex { // Sort edges by weight (filtration order) let mut sorted_edges: Vec<&SimplexEdge> = edges.iter().collect(); sorted_edges - .sort_unstable_by(|a, b| a.weight.partial_cmp(&b.weight).unwrap()); + .sort_unstable_by(|a, b| a.weight.partial_cmp(&b.weight).unwrap_or(std::cmp::Ordering::Equal)); for edge in sorted_edges { let pu = find(&mut parent, edge.u as usize); diff --git a/examples/exo-ai-2025/crates/exo-temporal/src/quantum_decay.rs b/examples/exo-ai-2025/crates/exo-temporal/src/quantum_decay.rs index 7f0753aa..2dec7a4b 100644 --- a/examples/exo-ai-2025/crates/exo-temporal/src/quantum_decay.rs +++ b/examples/exo-ai-2025/crates/exo-temporal/src/quantum_decay.rs @@ -135,7 +135,7 @@ impl QuantumDecayPool { fn evict_weakest(&mut self) { if let Some(idx) = self.patterns.iter() .enumerate() - .min_by(|a, b| a.1.decoherence_score().partial_cmp(&b.1.decoherence_score()).unwrap()) + .min_by(|a, b| a.1.decoherence_score().partial_cmp(&b.1.decoherence_score()).unwrap_or(std::cmp::Ordering::Equal)) .map(|(i, _)| i) { self.patterns.remove(idx);