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:
ruv 2026-04-26 01:17:12 -04:00
parent 4dd1f794d6
commit 5152bff1f4
2 changed files with 31 additions and 3 deletions

View file

@ -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)));

View file

@ -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