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