Files
ruvnet--RuView/v2/crates/wifi-densepose-bfld/src/embedding_ring.rs
T
ruv 60eaaa5af1 feat(adr-118/p2.2): EmbeddingRing 64-entry FIFO buffer — 53/53 GREEN
Iter 8. Lands the lifecycle half of ADR-120 §2.5: a bounded, in-place,
no_std-compatible ring of IdentityEmbeddings. Insertion is O(1); when
full, push evicts the oldest entry, whose Drop runs and zeroizes the
f32 storage. drain() clears the ring on the coherence-gate Recalibrate
action (ADR-121 §2.4).

Added:
- src/embedding_ring.rs (no_std-compatible; no heap):
  * EmbeddingRing struct with [Option<IdentityEmbedding>; RING_CAPACITY=64]
    backing array, head cursor, count
  * EmbeddingRing::new() / Default impl
  * push(emb) -> Option<IdentityEmbedding>  (evicted oldest when full)
  * len / is_empty / capacity / is_full / iter
  * iter() returns occupied slots in insertion order (oldest first)
  * drain() -> usize  (empties the ring, returns count drained)
- pub use EmbeddingRing, RING_CAPACITY from lib.rs

Uses `[const { None }; RING_CAPACITY]` (stable since 1.79) to initialize
the slot array for a non-Copy element type.

tests/embedding_ring.rs (9 named tests, all green):
  new_ring_is_empty
  default_constructor_matches_new
  push_below_capacity_returns_none
  iter_yields_in_insertion_order
  push_at_capacity_evicts_oldest_and_returns_it
    (verifies eviction reports the FIRST pushed value, not the last)
  push_beyond_capacity_keeps_last_n_entries
    (after 74 pushes into a 64-slot ring, the surviving 64 are positions 10..74)
  drain_empties_the_ring_and_returns_count
  drain_on_empty_ring_returns_zero
  ring_can_be_refilled_after_drain
    (post-drain push lands cleanly at index 0; iter yields exactly that entry)

ACs progressed:
- I2 ↑ — ring eviction and explicit drain both drop IdentityEmbeddings,
  which the iter-7 Drop impl zeroizes. The "in-RAM-only" lifecycle is now
  end-to-end: bounded buffer in, FIFO out, drain on Recalibrate.

Test config:
- cargo test --no-default-features → 31 passed (22 + 9)
- cargo test                       → 53 passed (44 + 9)

Out of scope (next iter target):
- PrivacyGate::demote(frame, target_class) — ADR-120 §2.4 monotonic class
  transition with field zeroization, refusing demote-to-Raw (compile-fail).
- SoulMatchOracle stub trait + no-op default impl (ADR-121 §2.6) so the
  Recalibrate exemption hook is wireable from `--features soul-signature`.

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-05-24 14:37:03 -04:00

106 lines
3.4 KiB
Rust

//! `EmbeddingRing` — bounded FIFO of `IdentityEmbedding`s.
//!
//! Holds at most [`RING_CAPACITY`] (default 64) embeddings. When full, `push`
//! evicts and returns the oldest entry so its `Drop` runs and the f32 storage
//! is zeroized. `drain()` is the explicit "rotate site_salt" hook from the
//! coherence-gate `Recalibrate` action (ADR-121 §2.4): it clears every slot
//! at once. The ring is `no_std`-compatible; no heap allocation.
use crate::embedding::IdentityEmbedding;
/// Default ring capacity — matches ADR-120 §2.5 ("ring buffer of 64 entries").
pub const RING_CAPACITY: usize = 64;
/// Fixed-capacity FIFO of identity embeddings. Insertion-ordered; oldest
/// evicted first when full.
pub struct EmbeddingRing {
slots: [Option<IdentityEmbedding>; RING_CAPACITY],
/// Index of the oldest slot — the next eviction target.
head: usize,
/// Number of currently-occupied slots (0..=RING_CAPACITY).
count: usize,
}
impl EmbeddingRing {
/// Build an empty ring.
#[must_use]
pub const fn new() -> Self {
Self {
slots: [const { None }; RING_CAPACITY],
head: 0,
count: 0,
}
}
/// Insert `emb`. If the ring is already full, evicts and returns the
/// oldest entry (its `Drop` runs as the returned `Option` is dropped).
pub fn push(&mut self, emb: IdentityEmbedding) -> Option<IdentityEmbedding> {
if self.count < RING_CAPACITY {
// Not full — write into the slot at head + count.
let idx = (self.head + self.count) % RING_CAPACITY;
self.slots[idx] = Some(emb);
self.count += 1;
None
} else {
// Full — overwrite the oldest slot, advance head.
let evicted = self.slots[self.head].take();
self.slots[self.head] = Some(emb);
self.head = (self.head + 1) % RING_CAPACITY;
evicted
}
}
/// Number of occupied slots.
#[must_use]
pub const fn len(&self) -> usize {
self.count
}
/// `true` iff `len() == 0`.
#[must_use]
pub const fn is_empty(&self) -> bool {
self.count == 0
}
/// Maximum number of slots — always [`RING_CAPACITY`].
#[must_use]
pub const fn capacity(&self) -> usize {
RING_CAPACITY
}
/// `true` iff `len() == capacity()`.
#[must_use]
pub const fn is_full(&self) -> bool {
self.count == RING_CAPACITY
}
/// Iterate occupied slots in **insertion order** (oldest first).
pub fn iter(&self) -> impl Iterator<Item = &IdentityEmbedding> + '_ {
(0..self.count).map(move |i| {
let idx = (self.head + i) % RING_CAPACITY;
self.slots[idx].as_ref().expect("occupied slot")
})
}
/// Empty the ring. Every contained `IdentityEmbedding` is dropped, which
/// zeroizes its storage. Returns the number of entries that were drained.
pub fn drain(&mut self) -> usize {
let drained = self.count;
for slot in &mut self.slots {
// Take() moves the embedding out; the temporary is dropped at the
// end of this statement, running IdentityEmbedding::drop which
// zeroes the f32 array.
let _ = slot.take();
}
self.head = 0;
self.count = 0;
drained
}
}
impl Default for EmbeddingRing {
fn default() -> Self {
Self::new()
}
}