mirror of
https://github.com/ruvnet/RuView.git
synced 2026-04-28 05:59:32 +00:00
hardening(ruvector,signal): L1+L3 from PR #435 review
Two follow-ups to the security review on PR #435: L1 — Defensive `if let Some(...)` for SketchBank::topk heap peek. The original `.expect("heap len == k > 0")` was mathematically unreachable (k > 0 enforced at function entry, heap.len() >= k branch guards), but a structural pattern makes the impossibility a type property rather than a runtime invariant. Same hot-path cost; zero panic risk in the production binary. L3 — Guard `embedding_dim == 0` in `EmbeddingHistory::novelty`. A 0-dim history is constructible via `with_sketch(0, ...)`; without the guard the function returned `NaN` (min_d as f32 / 0.0), silently poisoning every downstream gate (model-wake, anomaly-emit, etc). Now returns Some(1.0) — fail-loud at "no comparison possible → maximally novel," never NaN. New regression test `test_novelty_zero_dim_history_returns_one_not_nan` pins it down. Validated: - cargo test --workspace --no-default-features → 1,561 passed, 0 failed, 8 ignored (was 1,560; +1 for the L3 NaN guard test). - ESP32-S3 on COM7 streaming live CSI (cb #12400, RSSI fresh). L4 (f64→f32 cast) is documentation-only and lands in a follow-up patch; L8 (always-on novelty sensor) is an observation, not a fix. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
4dd1f794d6
commit
5152bff1f4
2 changed files with 31 additions and 3 deletions
|
|
@ -360,9 +360,13 @@ impl SketchBank {
|
|||
let d = sk.distance_unchecked(query);
|
||||
if heap.len() < k {
|
||||
heap.push(Reverse((d, *id)));
|
||||
} else {
|
||||
// Safe: heap has exactly k > 0 elements, just checked.
|
||||
let worst = heap.peek().expect("heap len == k > 0").0 .0;
|
||||
} else if let Some(&Reverse((worst, _))) = heap.peek() {
|
||||
// L1 hardening (PR #435 review): structural `if let` rather
|
||||
// than `.expect("heap len == k > 0")`. The branch is
|
||||
// mathematically unreachable when `heap.len() >= k > 0`,
|
||||
// but a defensive pattern makes the impossibility a type
|
||||
// property rather than a runtime invariant. Same hot-path
|
||||
// cost (one bounds check); zero panic risk.
|
||||
if d < worst {
|
||||
heap.pop();
|
||||
heap.push(Reverse((d, *id)));
|
||||
|
|
|
|||
|
|
@ -506,6 +506,14 @@ impl EmbeddingHistory {
|
|||
if self.sketches.is_empty() {
|
||||
return Some(1.0);
|
||||
}
|
||||
// L3 hardening (PR #435 security review): a 0-dim history would
|
||||
// produce `min_d as f32 / 0.0 = NaN`, silently poisoning every
|
||||
// downstream gate. `with_sketch(0, ...)` is constructible today;
|
||||
// treating "no comparison possible" as "maximally novel" is the
|
||||
// fail-loud behaviour every consumer of this score expects.
|
||||
if self.embedding_dim == 0 {
|
||||
return Some(1.0);
|
||||
}
|
||||
let q = wifi_densepose_ruvector::Sketch::from_embedding(query, sv);
|
||||
let min_d = self
|
||||
.sketches
|
||||
|
|
@ -935,6 +943,22 @@ mod tests {
|
|||
assert_eq!(h.novelty(&q), Some(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_novelty_zero_dim_history_returns_one_not_nan() {
|
||||
// L3 security-review finding (PR #435): a 0-dim sketch history is
|
||||
// constructible via `with_sketch(0, ...)`. Without the guard,
|
||||
// `novelty` would produce NaN (min_d / 0). This pins down the
|
||||
// documented fail-loud behaviour: 0-dim → max-novelty 1.0.
|
||||
let h = EmbeddingHistory::with_sketch(0, 100, 1);
|
||||
let q: Vec<f32> = vec![]; // 0-dim query is the only valid one here
|
||||
let result = h.novelty(&q);
|
||||
assert_eq!(result, Some(1.0), "0-dim history → max novelty, never NaN");
|
||||
assert!(
|
||||
!result.unwrap().is_nan(),
|
||||
"novelty must never be NaN — 0-dim is fail-loud, not silent"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_novelty_decreases_as_bank_grows_around_query() {
|
||||
// Insert progressively-closer-to-query embeddings; novelty must
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue