Skip to main content

ruvix_vecgraph/
coherence.rs

1//! Coherence tracking for kernel vector and graph stores.
2//!
3//! Coherence metadata is co-located with each vector and graph node,
4//! enabling the scheduler and proof engine to make informed decisions.
5//!
6//! # Design (from ADR-087 Section 4.3)
7//!
8//! - coherence_score: Structural consistency score (0.0-1.0)
9//! - last_mutation_epoch: Epoch of the last mutation
10//! - proof_attestation_hash: Hash of the proof that authorized the last mutation
11//!
12//! The coherence-aware scheduler uses these signals to:
13//! 1. Prioritize tasks processing genuinely new information
14//! 2. Deprioritize tasks whose mutations would lower coherence
15//! 3. Fast-path mutations within coherent partitions
16
17use ruvix_types::CoherenceMeta;
18
19/// Configuration for coherence tracking.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21#[repr(C)]
22pub struct CoherenceConfig {
23    /// Minimum acceptable coherence score (0-10000 = 0.0-1.0).
24    /// Operations that would lower coherence below this threshold may be rejected.
25    pub min_coherence_threshold: u16,
26
27    /// Whether to enable coherence-aware scheduling hints.
28    pub enable_scheduler_hints: bool,
29
30    /// Whether to track coherence deltas for mutation planning.
31    pub track_deltas: bool,
32
33    /// Decay rate for coherence over time (0-10000 = 0.0-1.0 per epoch).
34    /// Set to 0 for no decay.
35    pub decay_rate: u16,
36
37    /// Initial coherence score for new entries.
38    pub initial_coherence: u16,
39}
40
41impl Default for CoherenceConfig {
42    fn default() -> Self {
43        Self {
44            min_coherence_threshold: 5000, // 0.5
45            enable_scheduler_hints: true,
46            track_deltas: false,
47            decay_rate: 0,
48            initial_coherence: 10000, // 1.0 = fully coherent
49        }
50    }
51}
52
53impl CoherenceConfig {
54    /// Creates a new coherence configuration.
55    #[inline]
56    #[must_use]
57    pub const fn new() -> Self {
58        Self {
59            min_coherence_threshold: 5000,
60            enable_scheduler_hints: true,
61            track_deltas: false,
62            decay_rate: 0,
63            initial_coherence: 10000,
64        }
65    }
66
67    /// Sets the minimum coherence threshold.
68    #[inline]
69    #[must_use]
70    pub const fn with_min_threshold(mut self, threshold: f32) -> Self {
71        self.min_coherence_threshold = (threshold.clamp(0.0, 1.0) * 10000.0) as u16;
72        self
73    }
74
75    /// Enables or disables scheduler hints.
76    #[inline]
77    #[must_use]
78    pub const fn with_scheduler_hints(mut self, enabled: bool) -> Self {
79        self.enable_scheduler_hints = enabled;
80        self
81    }
82
83    /// Enables or disables delta tracking.
84    #[inline]
85    #[must_use]
86    pub const fn with_delta_tracking(mut self, enabled: bool) -> Self {
87        self.track_deltas = enabled;
88        self
89    }
90
91    /// Sets the coherence decay rate.
92    #[inline]
93    #[must_use]
94    pub const fn with_decay_rate(mut self, rate: f32) -> Self {
95        self.decay_rate = (rate.clamp(0.0, 1.0) * 10000.0) as u16;
96        self
97    }
98
99    /// Returns the minimum threshold as a float.
100    #[inline]
101    #[must_use]
102    pub fn min_threshold_f32(&self) -> f32 {
103        self.min_coherence_threshold as f32 / 10000.0
104    }
105}
106
107/// Tracks coherence state for a collection of entries.
108#[derive(Debug, Clone)]
109pub struct CoherenceTracker {
110    /// Configuration for coherence tracking.
111    config: CoherenceConfig,
112
113    /// Global epoch counter for mutations.
114    current_epoch: u64,
115
116    /// Rolling average coherence score.
117    average_coherence: u16,
118
119    /// Number of entries tracked.
120    entry_count: u32,
121
122    /// Sum of all coherence scores (for average calculation).
123    coherence_sum: u64,
124
125    /// Number of mutations below threshold.
126    low_coherence_mutations: u32,
127}
128
129impl CoherenceTracker {
130    /// Creates a new coherence tracker with the given configuration.
131    #[inline]
132    #[must_use]
133    pub const fn new(config: CoherenceConfig) -> Self {
134        Self {
135            config,
136            current_epoch: 0,
137            average_coherence: 10000,
138            entry_count: 0,
139            coherence_sum: 0,
140            low_coherence_mutations: 0,
141        }
142    }
143
144    /// Returns the current epoch.
145    #[inline]
146    #[must_use]
147    pub const fn current_epoch(&self) -> u64 {
148        self.current_epoch
149    }
150
151    /// Advances the epoch and returns the new value.
152    #[inline]
153    pub fn advance_epoch(&mut self) -> u64 {
154        self.current_epoch = self.current_epoch.wrapping_add(1);
155        self.current_epoch
156    }
157
158    /// Returns the configuration.
159    #[inline]
160    #[must_use]
161    pub const fn config(&self) -> &CoherenceConfig {
162        &self.config
163    }
164
165    /// Creates initial coherence metadata for a new entry.
166    ///
167    /// Note: Uses current_epoch + 1 to ensure new entries always have
168    /// a non-zero mutation_epoch, signifying they've been mutated at least once.
169    #[inline]
170    #[must_use]
171    pub fn create_initial_meta(&mut self, proof_attestation_hash: [u8; 32]) -> CoherenceMeta {
172        CoherenceMeta::new(
173            self.config.initial_coherence,
174            self.advance_epoch(),
175            proof_attestation_hash,
176        )
177    }
178
179    /// Updates coherence tracking when an entry is added.
180    pub fn on_entry_added(&mut self, coherence_score: u16) {
181        self.entry_count = self.entry_count.saturating_add(1);
182        self.coherence_sum = self.coherence_sum.saturating_add(coherence_score as u64);
183        self.update_average();
184    }
185
186    /// Updates coherence tracking when an entry is removed.
187    pub fn on_entry_removed(&mut self, coherence_score: u16) {
188        self.entry_count = self.entry_count.saturating_sub(1);
189        self.coherence_sum = self.coherence_sum.saturating_sub(coherence_score as u64);
190        self.update_average();
191    }
192
193    /// Updates coherence tracking when an entry is mutated.
194    ///
195    /// Returns the new coherence metadata for the entry.
196    pub fn on_entry_mutated(
197        &mut self,
198        old_meta: &CoherenceMeta,
199        new_coherence: u16,
200        proof_attestation_hash: [u8; 32],
201    ) -> CoherenceMeta {
202        // Update sum
203        self.coherence_sum = self
204            .coherence_sum
205            .saturating_sub(old_meta.coherence_score as u64)
206            .saturating_add(new_coherence as u64);
207
208        self.update_average();
209
210        // Track low coherence mutations
211        if new_coherence < self.config.min_coherence_threshold {
212            self.low_coherence_mutations = self.low_coherence_mutations.saturating_add(1);
213        }
214
215        // Create new metadata
216        CoherenceMeta::new(new_coherence, self.advance_epoch(), proof_attestation_hash)
217    }
218
219    /// Checks if a proposed coherence change would violate constraints.
220    #[inline]
221    #[must_use]
222    pub fn would_violate_threshold(&self, proposed_coherence: u16) -> bool {
223        proposed_coherence < self.config.min_coherence_threshold
224    }
225
226    /// Returns the average coherence score as a float.
227    #[inline]
228    #[must_use]
229    pub fn average_coherence_f32(&self) -> f32 {
230        self.average_coherence as f32 / 10000.0
231    }
232
233    /// Returns the number of entries tracked.
234    #[inline]
235    #[must_use]
236    pub const fn entry_count(&self) -> u32 {
237        self.entry_count
238    }
239
240    /// Returns the number of low-coherence mutations.
241    #[inline]
242    #[must_use]
243    pub const fn low_coherence_mutations(&self) -> u32 {
244        self.low_coherence_mutations
245    }
246
247    /// Updates the rolling average coherence.
248    fn update_average(&mut self) {
249        if self.entry_count > 0 {
250            self.average_coherence = (self.coherence_sum / self.entry_count as u64) as u16;
251        } else {
252            self.average_coherence = self.config.initial_coherence;
253        }
254    }
255
256    /// Applies coherence decay based on elapsed epochs.
257    ///
258    /// This is called periodically to age coherence scores.
259    pub fn apply_decay(&mut self, epochs_elapsed: u64) {
260        if self.config.decay_rate == 0 || epochs_elapsed == 0 {
261            return;
262        }
263
264        // Calculate decay factor: (1 - decay_rate) ^ epochs_elapsed
265        // Simplified: subtract decay_rate * epochs_elapsed from average
266        let decay_amount =
267            ((self.config.decay_rate as u64) * epochs_elapsed).min(self.average_coherence as u64);
268
269        self.average_coherence = self.average_coherence.saturating_sub(decay_amount as u16);
270
271        // Also decay the sum proportionally
272        if self.entry_count > 0 {
273            let sum_decay = decay_amount * self.entry_count as u64;
274            self.coherence_sum = self.coherence_sum.saturating_sub(sum_decay);
275        }
276    }
277}
278
279impl Default for CoherenceTracker {
280    fn default() -> Self {
281        Self::new(CoherenceConfig::default())
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_coherence_config_defaults() {
291        let config = CoherenceConfig::default();
292        assert_eq!(config.initial_coherence, 10000);
293        assert_eq!(config.min_coherence_threshold, 5000);
294        assert!(config.enable_scheduler_hints);
295    }
296
297    #[test]
298    fn test_coherence_config_builder() {
299        let config = CoherenceConfig::new()
300            .with_min_threshold(0.7)
301            .with_decay_rate(0.01)
302            .with_delta_tracking(true);
303
304        assert_eq!(config.min_coherence_threshold, 7000);
305        assert_eq!(config.decay_rate, 100);
306        assert!(config.track_deltas);
307    }
308
309    #[test]
310    fn test_coherence_tracker_epoch() {
311        let mut tracker = CoherenceTracker::default();
312
313        assert_eq!(tracker.current_epoch(), 0);
314
315        let epoch1 = tracker.advance_epoch();
316        assert_eq!(epoch1, 1);
317
318        let epoch2 = tracker.advance_epoch();
319        assert_eq!(epoch2, 2);
320    }
321
322    #[test]
323    fn test_coherence_tracker_average() {
324        let mut tracker = CoherenceTracker::default();
325
326        // Add entries with different coherence
327        tracker.on_entry_added(10000); // 1.0
328        tracker.on_entry_added(8000); // 0.8
329        tracker.on_entry_added(6000); // 0.6
330
331        // Average should be (10000 + 8000 + 6000) / 3 = 8000
332        assert_eq!(tracker.entry_count(), 3);
333        assert!((tracker.average_coherence_f32() - 0.8).abs() < 0.01);
334    }
335
336    #[test]
337    fn test_coherence_tracker_mutation() {
338        let mut tracker = CoherenceTracker::default();
339
340        let initial_meta = tracker.create_initial_meta([0u8; 32]);
341        tracker.on_entry_added(initial_meta.coherence_score);
342
343        assert_eq!(initial_meta.coherence_score, 10000);
344        assert_eq!(initial_meta.mutation_epoch, 1); // Epoch 1 for new entry
345
346        let new_meta = tracker.on_entry_mutated(&initial_meta, 9000, [1u8; 32]);
347
348        assert_eq!(new_meta.coherence_score, 9000);
349        assert_eq!(new_meta.mutation_epoch, 2); // Epoch 2 after mutation
350        assert_eq!(new_meta.proof_attestation_hash, [1u8; 32]);
351    }
352
353    #[test]
354    fn test_coherence_threshold_violation() {
355        let tracker = CoherenceTracker::new(CoherenceConfig::new().with_min_threshold(0.5));
356
357        assert!(!tracker.would_violate_threshold(6000)); // 0.6 > 0.5
358        assert!(tracker.would_violate_threshold(4000)); // 0.4 < 0.5
359    }
360
361    #[test]
362    fn test_coherence_decay() {
363        let config = CoherenceConfig::new().with_decay_rate(0.1); // 10% decay per epoch
364        let mut tracker = CoherenceTracker::new(config);
365
366        tracker.on_entry_added(10000);
367        assert_eq!(tracker.average_coherence, 10000);
368
369        tracker.apply_decay(1);
370        // Should decay by 10% = 1000
371        assert_eq!(tracker.average_coherence, 9000);
372    }
373}