Skip to main content

ruvector_cognitive_container/
container.rs

1use serde::{Deserialize, Serialize};
2
3use crate::epoch::{ContainerEpochBudget, EpochController, Phase};
4use crate::error::{ContainerError, Result};
5use crate::memory::{MemoryConfig, MemorySlab};
6use crate::witness::{CoherenceDecision, ContainerWitnessReceipt, VerificationResult, WitnessChain};
7
8/// Top-level container configuration.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ContainerConfig {
11    /// Memory layout.
12    pub memory: MemoryConfig,
13    /// Per-epoch tick budgets.
14    pub epoch_budget: ContainerEpochBudget,
15    /// Unique identifier for this container instance.
16    pub instance_id: u64,
17    /// Maximum number of witness receipts retained.
18    pub max_receipts: usize,
19}
20
21impl Default for ContainerConfig {
22    fn default() -> Self {
23        Self {
24            memory: MemoryConfig::default(),
25            epoch_budget: ContainerEpochBudget::default(),
26            instance_id: 0,
27            max_receipts: 1024,
28        }
29    }
30}
31
32/// A graph-structure delta to apply during the ingest phase.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub enum Delta {
35    EdgeAdd { u: usize, v: usize, weight: f64 },
36    EdgeRemove { u: usize, v: usize },
37    WeightUpdate { u: usize, v: usize, new_weight: f64 },
38    Observation { node: usize, value: f64 },
39}
40
41/// Bitmask tracking which pipeline components completed during a tick.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub struct ComponentMask(pub u8);
44
45impl ComponentMask {
46    pub const INGEST: Self = Self(0b0000_0001);
47    pub const MINCUT: Self = Self(0b0000_0010);
48    pub const SPECTRAL: Self = Self(0b0000_0100);
49    pub const EVIDENCE: Self = Self(0b0000_1000);
50    pub const WITNESS: Self = Self(0b0001_0000);
51    pub const ALL: Self = Self(0b0001_1111);
52
53    /// Returns `true` if all bits in `other` are set in `self`.
54    pub fn contains(&self, other: Self) -> bool {
55        self.0 & other.0 == other.0
56    }
57
58    /// Set all bits present in `other`.
59    pub fn insert(&mut self, other: Self) {
60        self.0 |= other.0;
61    }
62}
63
64/// Output of a single `tick()` invocation.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct TickResult {
67    /// The witness receipt generated for this epoch.
68    pub receipt: ContainerWitnessReceipt,
69    /// True if any pipeline phase was skipped due to budget exhaustion.
70    pub partial: bool,
71    /// Bitmask of completed components.
72    pub components_completed: u8,
73    /// Wall-clock duration in microseconds.
74    pub tick_time_us: u64,
75}
76
77/// Internal graph representation.
78struct GraphState {
79    num_vertices: usize,
80    num_edges: usize,
81    edges: Vec<(usize, usize, f64)>,
82    min_cut_value: f64,
83    canonical_hash: [u8; 32],
84}
85
86impl GraphState {
87    fn new() -> Self {
88        Self {
89            num_vertices: 0,
90            num_edges: 0,
91            edges: Vec::new(),
92            min_cut_value: 0.0,
93            canonical_hash: [0u8; 32],
94        }
95    }
96}
97
98/// Internal spectral analysis state.
99struct SpectralState {
100    scs: f64,
101    fiedler: f64,
102    gap: f64,
103}
104
105impl SpectralState {
106    fn new() -> Self {
107        Self {
108            scs: 0.0,
109            fiedler: 0.0,
110            gap: 0.0,
111        }
112    }
113}
114
115/// Internal evidence accumulation state.
116struct EvidenceState {
117    observations: Vec<f64>,
118    accumulated_evidence: f64,
119    threshold: f64,
120}
121
122impl EvidenceState {
123    fn new() -> Self {
124        Self {
125            observations: Vec::new(),
126            accumulated_evidence: 0.0,
127            threshold: 1.0,
128        }
129    }
130}
131
132/// Serializable snapshot of the container state.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct ContainerSnapshot {
135    pub epoch: u64,
136    pub config: ContainerConfig,
137    pub graph_edges: Vec<(usize, usize, f64)>,
138    pub spectral_scs: f64,
139    pub evidence_accumulated: f64,
140}
141
142/// A sealed cognitive container that orchestrates ingest, min-cut, spectral,
143/// evidence, and witness phases within a memory slab and epoch budget.
144pub struct CognitiveContainer {
145    config: ContainerConfig,
146    #[allow(dead_code)]
147    slab: MemorySlab,
148    epoch: EpochController,
149    witness: WitnessChain,
150    graph: GraphState,
151    spectral: SpectralState,
152    evidence: EvidenceState,
153    initialized: bool,
154}
155
156impl CognitiveContainer {
157    /// Create and initialize a new container.
158    pub fn new(config: ContainerConfig) -> Result<Self> {
159        let slab = MemorySlab::new(config.memory.clone())?;
160        let epoch = EpochController::new(config.epoch_budget.clone());
161        let witness = WitnessChain::new(config.max_receipts);
162
163        Ok(Self {
164            config,
165            slab,
166            epoch,
167            witness,
168            graph: GraphState::new(),
169            spectral: SpectralState::new(),
170            evidence: EvidenceState::new(),
171            initialized: true,
172        })
173    }
174
175    /// Execute one full epoch: ingest deltas, recompute min-cut, update spectral
176    /// metrics, accumulate evidence, and produce a witness receipt.
177    pub fn tick(&mut self, deltas: &[Delta]) -> Result<TickResult> {
178        if !self.initialized {
179            return Err(ContainerError::NotInitialized);
180        }
181
182        let start = std::time::Instant::now();
183        self.epoch.reset();
184        let mut completed = ComponentMask(0);
185
186        // Phase 1: Ingest
187        if self.epoch.try_budget(Phase::Ingest) {
188            for delta in deltas {
189                self.apply_delta(delta);
190            }
191            self.epoch.consume(deltas.len().max(1) as u64);
192            completed.insert(ComponentMask::INGEST);
193        }
194
195        // Phase 2: Min-cut
196        if self.epoch.try_budget(Phase::MinCut) {
197            self.recompute_mincut();
198            self.epoch.consume(self.graph.num_edges.max(1) as u64);
199            completed.insert(ComponentMask::MINCUT);
200        }
201
202        // Phase 3: Spectral
203        if self.epoch.try_budget(Phase::Spectral) {
204            self.update_spectral();
205            self.epoch.consume(self.graph.num_vertices.max(1) as u64);
206            completed.insert(ComponentMask::SPECTRAL);
207        }
208
209        // Phase 4: Evidence
210        if self.epoch.try_budget(Phase::Evidence) {
211            self.accumulate_evidence();
212            self.epoch.consume(self.evidence.observations.len().max(1) as u64);
213            completed.insert(ComponentMask::EVIDENCE);
214        }
215
216        // Phase 5: Witness
217        let decision = self.make_decision();
218        let input_bytes = self.serialize_deltas(deltas);
219        let mincut_bytes = self.graph.min_cut_value.to_le_bytes();
220        let evidence_bytes = self.evidence.accumulated_evidence.to_le_bytes();
221
222        let receipt = self.witness.generate_receipt(
223            &input_bytes,
224            &mincut_bytes,
225            self.spectral.scs,
226            &evidence_bytes,
227            decision,
228        );
229        completed.insert(ComponentMask::WITNESS);
230
231        Ok(TickResult {
232            receipt,
233            partial: completed.0 != ComponentMask::ALL.0,
234            components_completed: completed.0,
235            tick_time_us: start.elapsed().as_micros() as u64,
236        })
237    }
238
239    /// Reference to the container configuration.
240    pub fn config(&self) -> &ContainerConfig {
241        &self.config
242    }
243
244    /// Current epoch counter (next epoch to be generated).
245    pub fn current_epoch(&self) -> u64 {
246        self.witness.current_epoch()
247    }
248
249    /// Slice of all retained witness receipts.
250    pub fn receipt_chain(&self) -> &[ContainerWitnessReceipt] {
251        self.witness.receipt_chain()
252    }
253
254    /// Verify the integrity of the internal witness chain.
255    pub fn verify_chain(&self) -> VerificationResult {
256        WitnessChain::verify_chain(self.witness.receipt_chain())
257    }
258
259    /// Produce a serializable snapshot of the current container state.
260    pub fn snapshot(&self) -> ContainerSnapshot {
261        ContainerSnapshot {
262            epoch: self.witness.current_epoch(),
263            config: self.config.clone(),
264            graph_edges: self.graph.edges.clone(),
265            spectral_scs: self.spectral.scs,
266            evidence_accumulated: self.evidence.accumulated_evidence,
267        }
268    }
269
270    // ---- Private helpers ----
271
272    fn apply_delta(&mut self, delta: &Delta) {
273        match delta {
274            Delta::EdgeAdd { u, v, weight } => {
275                self.graph.edges.push((*u, *v, *weight));
276                self.graph.num_edges += 1;
277                let max_node = (*u).max(*v) + 1;
278                if max_node > self.graph.num_vertices {
279                    self.graph.num_vertices = max_node;
280                }
281            }
282            Delta::EdgeRemove { u, v } => {
283                self.graph.edges.retain(|(a, b, _)| !(*a == *u && *b == *v));
284                self.graph.num_edges = self.graph.edges.len();
285            }
286            Delta::WeightUpdate { u, v, new_weight } => {
287                for edge in &mut self.graph.edges {
288                    if edge.0 == *u && edge.1 == *v {
289                        edge.2 = *new_weight;
290                    }
291                }
292            }
293            Delta::Observation { value, .. } => {
294                self.evidence.observations.push(*value);
295            }
296        }
297    }
298
299    /// Simplified Stoer-Wagner-style min-cut: find the minimum total weight
300    /// among all vertex partitions. For small graphs this uses the minimum
301    /// weighted vertex degree as a fast approximation.
302    fn recompute_mincut(&mut self) {
303        if self.graph.edges.is_empty() {
304            self.graph.min_cut_value = 0.0;
305            self.graph.canonical_hash = [0u8; 32];
306            return;
307        }
308
309        // Approximate min-cut via minimum weighted degree.
310        let n = self.graph.num_vertices;
311        let mut degree = vec![0.0f64; n];
312        for &(u, v, w) in &self.graph.edges {
313            if u < n {
314                degree[u] += w;
315            }
316            if v < n {
317                degree[v] += w;
318            }
319        }
320
321        self.graph.min_cut_value = degree
322            .iter()
323            .copied()
324            .filter(|&d| d > 0.0)
325            .fold(f64::MAX, f64::min);
326        if self.graph.min_cut_value == f64::MAX {
327            self.graph.min_cut_value = 0.0;
328        }
329
330        // Canonical hash: hash sorted edges.
331        let mut sorted = self.graph.edges.clone();
332        sorted.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));
333        let bytes: Vec<u8> = sorted
334            .iter()
335            .flat_map(|(u, v, w)| {
336                let mut b = Vec::with_capacity(24);
337                b.extend_from_slice(&u.to_le_bytes());
338                b.extend_from_slice(&v.to_le_bytes());
339                b.extend_from_slice(&w.to_le_bytes());
340                b
341            })
342            .collect();
343        self.graph.canonical_hash = crate::witness::deterministic_hash_public(&bytes);
344    }
345
346    /// Simplified spectral metrics: SCS is the ratio of min-cut to total weight.
347    fn update_spectral(&mut self) {
348        let total_weight: f64 = self.graph.edges.iter().map(|e| e.2).sum();
349        if total_weight > 0.0 {
350            self.spectral.scs = self.graph.min_cut_value / total_weight;
351            self.spectral.fiedler = self.spectral.scs;
352            self.spectral.gap = 1.0 - self.spectral.scs;
353        } else {
354            self.spectral.scs = 0.0;
355            self.spectral.fiedler = 0.0;
356            self.spectral.gap = 0.0;
357        }
358    }
359
360    /// Simple sequential probability ratio test (SPRT) style accumulation.
361    fn accumulate_evidence(&mut self) {
362        if self.evidence.observations.is_empty() {
363            return;
364        }
365        let mean: f64 =
366            self.evidence.observations.iter().sum::<f64>() / self.evidence.observations.len() as f64;
367        self.evidence.accumulated_evidence += mean.abs();
368    }
369
370    /// Decision logic based on spectral coherence and accumulated evidence.
371    fn make_decision(&self) -> CoherenceDecision {
372        if self.graph.edges.is_empty() {
373            return CoherenceDecision::Inconclusive;
374        }
375        if self.spectral.scs >= 0.5 && self.evidence.accumulated_evidence < self.evidence.threshold {
376            return CoherenceDecision::Pass;
377        }
378        if self.spectral.scs < 0.2 {
379            let severity = ((1.0 - self.spectral.scs) * 10.0).min(255.0) as u8;
380            return CoherenceDecision::Fail { severity };
381        }
382        CoherenceDecision::Inconclusive
383    }
384
385    fn serialize_deltas(&self, deltas: &[Delta]) -> Vec<u8> {
386        serde_json::to_vec(deltas).unwrap_or_default()
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    fn default_container() -> CognitiveContainer {
395        CognitiveContainer::new(ContainerConfig::default()).unwrap()
396    }
397
398    #[test]
399    fn test_container_lifecycle() {
400        let mut container = default_container();
401        assert_eq!(container.current_epoch(), 0);
402
403        let result = container.tick(&[]).unwrap();
404        assert_eq!(result.receipt.epoch, 0);
405        assert_eq!(container.current_epoch(), 1);
406
407        match container.verify_chain() {
408            VerificationResult::Valid { chain_length, .. } => {
409                assert_eq!(chain_length, 1);
410            }
411            other => panic!("Expected Valid, got {other:?}"),
412        }
413    }
414
415    #[test]
416    fn test_container_tick_with_deltas() {
417        let mut container = default_container();
418
419        let deltas = vec![
420            Delta::EdgeAdd { u: 0, v: 1, weight: 1.0 },
421            Delta::EdgeAdd { u: 1, v: 2, weight: 2.0 },
422            Delta::EdgeAdd { u: 2, v: 0, weight: 1.5 },
423            Delta::Observation { node: 0, value: 0.8 },
424        ];
425
426        let result = container.tick(&deltas).unwrap();
427        assert!(!result.partial);
428        assert_eq!(result.components_completed, ComponentMask::ALL.0);
429
430        // Graph should reflect the edges.
431        let snap = container.snapshot();
432        assert_eq!(snap.graph_edges.len(), 3);
433        assert!(snap.spectral_scs > 0.0);
434    }
435
436    #[test]
437    fn test_container_snapshot_restore() {
438        let mut container = default_container();
439        container.tick(&[
440            Delta::EdgeAdd { u: 0, v: 1, weight: 3.0 },
441        ]).unwrap();
442
443        let snap = container.snapshot();
444        let json = serde_json::to_string(&snap).expect("serialize snapshot");
445        let restored: ContainerSnapshot =
446            serde_json::from_str(&json).expect("deserialize snapshot");
447
448        assert_eq!(restored.epoch, snap.epoch);
449        assert_eq!(restored.graph_edges.len(), snap.graph_edges.len());
450        assert!((restored.spectral_scs - snap.spectral_scs).abs() < f64::EPSILON);
451    }
452
453    #[test]
454    fn test_container_decision_logic() {
455        let mut container = default_container();
456
457        // Empty graph => Inconclusive
458        let r = container.tick(&[]).unwrap();
459        assert_eq!(r.receipt.decision, CoherenceDecision::Inconclusive);
460
461        // Single edge: min-cut/total = 1.0 (high scs), no evidence => Pass
462        let r = container.tick(&[
463            Delta::EdgeAdd { u: 0, v: 1, weight: 5.0 },
464        ]).unwrap();
465        assert_eq!(r.receipt.decision, CoherenceDecision::Pass);
466    }
467
468    #[test]
469    fn test_container_multiple_epochs() {
470        let mut container = default_container();
471        for i in 0..10 {
472            container.tick(&[
473                Delta::EdgeAdd { u: i, v: i + 1, weight: 1.0 },
474            ]).unwrap();
475        }
476        assert_eq!(container.current_epoch(), 10);
477
478        match container.verify_chain() {
479            VerificationResult::Valid {
480                chain_length,
481                first_epoch,
482                last_epoch,
483            } => {
484                assert_eq!(chain_length, 10);
485                assert_eq!(first_epoch, 0);
486                assert_eq!(last_epoch, 9);
487            }
488            other => panic!("Expected Valid, got {other:?}"),
489        }
490    }
491
492    #[test]
493    fn test_container_edge_remove() {
494        let mut container = default_container();
495        container.tick(&[
496            Delta::EdgeAdd { u: 0, v: 1, weight: 1.0 },
497            Delta::EdgeAdd { u: 1, v: 2, weight: 2.0 },
498        ]).unwrap();
499
500        container.tick(&[
501            Delta::EdgeRemove { u: 0, v: 1 },
502        ]).unwrap();
503
504        let snap = container.snapshot();
505        assert_eq!(snap.graph_edges.len(), 1);
506        assert_eq!(snap.graph_edges[0], (1, 2, 2.0));
507    }
508
509    #[test]
510    fn test_container_weight_update() {
511        let mut container = default_container();
512        container.tick(&[
513            Delta::EdgeAdd { u: 0, v: 1, weight: 1.0 },
514        ]).unwrap();
515
516        container.tick(&[
517            Delta::WeightUpdate { u: 0, v: 1, new_weight: 5.0 },
518        ]).unwrap();
519
520        let snap = container.snapshot();
521        assert_eq!(snap.graph_edges[0].2, 5.0);
522    }
523
524    #[test]
525    fn test_component_mask() {
526        let mut mask = ComponentMask(0);
527        assert!(!mask.contains(ComponentMask::INGEST));
528
529        mask.insert(ComponentMask::INGEST);
530        assert!(mask.contains(ComponentMask::INGEST));
531        assert!(!mask.contains(ComponentMask::MINCUT));
532
533        mask.insert(ComponentMask::MINCUT);
534        assert!(mask.contains(ComponentMask::INGEST));
535        assert!(mask.contains(ComponentMask::MINCUT));
536
537        assert!(!mask.contains(ComponentMask::ALL));
538    }
539}