soma-som-core 0.1.0

Universal soma(som) structural primitives — Quad / Tree / Ring / Genesis / Fingerprint / TemporalLedger / CrossingRecord
Documentation
// SPDX-License-Identifier: LGPL-3.0-only
#![allow(missing_docs)]

//! The SOMA Ring: six units, 30 Quads, ring state machine.
//!
//! ## Spec traceability
//! - Spec §3: Ring derivation and structure
//! - Spec §4: Two-axis structure (vertical SOMA, horizontal SOM)
//! - Spec §6: Ring transitions and cycle semantics
//! - Spec §7: Structural invariants (ring closure, cycle closure)

use serde::{Deserialize, Serialize};

use crate::error::SomaError;
use crate::quad::Quad;
use crate::types::{Layer, UnitId};

/// The operational state of the ring.
///
/// ## Spec §10
/// - `Uninitialized`: before genesis
/// - `Genesis`: during the genesis pipeline (t = 0)
/// - `Ring`: standard operation (t > 0), closed loop
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RingState {
    /// No genesis has occurred. The ring does not exist yet.
    Uninitialized,
    /// Genesis cycle in progress. The ring operates as a directed pipeline.
    /// The HU→FU link is open.
    Genesis,
    /// Standard operation. The ring is closed. t > 0.
    Ring,
}

/// A unit's complete state: one SOMA Quad + four SOM Quads.
///
/// ## Spec §5
///
/// Each unit has:
/// - 1 SOMA Quad (vertical axis, linking units)
/// - 4 SOM Quads (horizontal axis: Data, Server, Client, Interface)
///
/// Total per unit: 5 Quads, 15 key positions.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnitState {
    /// The unit's identity.
    pub unit_id: UnitId,

    /// The SOMA Quad: unit-level structural state on the vertical axis.
    /// Carries the unit's ring-level identity (Spec §8.3).
    pub soma_quad: Quad,

    /// The four SOM layer Quads, indexed by `Layer`.
    /// `som_quads[0]` = Data, `som_quads[1]` = Server,
    /// `som_quads[2]` = Client, `som_quads[3]` = Interface.
    pub som_quads: [Quad; 4],
}

impl UnitState {
    /// Create a new UnitState with empty Quads.
    pub fn new(unit_id: UnitId) -> Self {
        Self {
            unit_id,
            soma_quad: Quad::empty(),
            som_quads: [Quad::empty(), Quad::empty(), Quad::empty(), Quad::empty()],
        }
    }

    /// Get a reference to the SOM Quad at the given layer.
    pub fn som_quad(&self, layer: Layer) -> &Quad {
        #[allow(clippy::indexing_slicing)] // Layer enum is bounded; index always valid
        &self.som_quads[layer.index()]
    }

    /// Get a mutable reference to the SOM Quad at the given layer.
    pub fn som_quad_mut(&mut self, layer: Layer) -> &mut Quad {
        #[allow(clippy::indexing_slicing)] // Layer enum is bounded; index always valid
        &mut self.som_quads[layer.index()]
    }
}

/// The complete SOMA ring: all six units with their state.
///
/// ## Structural constants (Spec §14.1)
/// - 6 units, 30 Quads, 90 key positions
/// - Ring direction: FU → MU → CU → OU → SU → HU → FU
///
/// ## Invariants this type helps enforce
/// - **Invariant 2 (Ring closure)**: The ring always has exactly 6 units.
/// - **Invariant 7 (Cycle closure)**: The `state` field tracks whether HU→FU is active.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Ring {
    /// The six unit states, indexed by `UnitId`.
    units: [UnitState; 6],

    /// Current ring operational state.
    pub state: RingState,

    /// Current cycle index. 0 during genesis, incremented after each complete cycle.
    pub cycle_index: u64,
}

impl Ring {
    /// Create a new uninitialized ring with empty state.
    pub fn new() -> Self {
        Self {
            units: [
                UnitState::new(UnitId::FU),
                UnitState::new(UnitId::MU),
                UnitState::new(UnitId::CU),
                UnitState::new(UnitId::OU),
                UnitState::new(UnitId::SU),
                UnitState::new(UnitId::HU),
            ],
            state: RingState::Uninitialized,
            cycle_index: 0,
        }
    }

    /// Get a reference to a unit's state.
    pub fn unit(&self, id: UnitId) -> &UnitState {
        #[allow(clippy::indexing_slicing)] // UnitId enum is bounded; index always valid
        &self.units[id.index()]
    }

    /// Get a mutable reference to a unit's state.
    pub fn unit_mut(&mut self, id: UnitId) -> &mut UnitState {
        #[allow(clippy::indexing_slicing)] // UnitId enum is bounded; index always valid
        &mut self.units[id.index()]
    }

    /// Collect all 72 SOM Quads in canonical position order.
    ///
    /// Order: FU.Data, FU.Server, FU.Client, FU.Interface,
    ///        MU.Data, ... HU.Interface
    ///
    /// Used for awareness fingerprint computation (Spec Definition 7).
    pub fn all_som_quads(&self) -> Vec<&Quad> {
        let mut quads = Vec::with_capacity(24);
        for &unit in &UnitId::ALL {
            for &layer in &Layer::ALL {
                quads.push(self.unit(unit).som_quad(layer));
            }
        }
        quads
    }

    /// Collect all 6 SOMA Quads in canonical order.
    pub fn all_soma_quads(&self) -> Vec<&Quad> {
        UnitId::ALL
            .iter()
            .map(|&u| &self.unit(u).soma_quad)
            .collect()
    }

    /// Collect all 30 Quads: 24 SOM + 6 SOMA, in canonical order.
    pub fn all_quads(&self) -> Vec<&Quad> {
        let mut quads = self.all_som_quads();
        quads.extend(self.all_soma_quads());
        quads
    }

    /// Validate that a transition from `source` to `destination` is legal.
    ///
    /// ## Invariant 5 (No bypass)
    /// The only legal vertical transitions are σ(source) = destination.
    ///
    /// ## Invariant 7 (Cycle closure)
    /// HU → FU is only legal when `state == Ring` (not during genesis pipeline
    /// until closure).
    pub fn validate_transition(
        &self,
        source: UnitId,
        destination: UnitId,
    ) -> Result<(), SomaError> {
        let expected = source.successor();
        if destination != expected {
            return Err(SomaError::InvalidTransition {
                from_unit: source,
                to_unit: destination,
                expected,
            });
        }

        // During genesis, all transitions including HU→FU are allowed
        // (the pipeline traverses linearly, then closes)
        if self.state == RingState::Uninitialized {
            return Err(SomaError::RingNotInitialized);
        }

        Ok(())
    }

    /// Check if all units have non-empty state (basic liveness check).
    pub fn is_populated(&self) -> bool {
        self.units
            .iter()
            .all(|u| !u.soma_quad.is_empty() || u.som_quads.iter().any(|q| !q.is_empty()))
    }
}

impl Default for Ring {
    fn default() -> Self {
        Self::new()
    }
}

// inline: exercises module-private items via super::*
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn new_ring_is_uninitialized() {
        let ring = Ring::new();
        assert_eq!(ring.state, RingState::Uninitialized);
        assert_eq!(ring.cycle_index, 0);
    }

    #[test]
    fn ring_has_six_units() {
        let ring = Ring::new();
        for &unit in &UnitId::ALL {
            assert_eq!(ring.unit(unit).unit_id, unit);
        }
    }

    #[test]
    fn all_som_quads_returns_24() {
        let ring = Ring::new();
        assert_eq!(ring.all_som_quads().len(), 24);
    }

    #[test]
    fn all_soma_quads_returns_6() {
        let ring = Ring::new();
        assert_eq!(ring.all_soma_quads().len(), 6);
    }

    #[test]
    fn all_quads_returns_30() {
        let ring = Ring::new();
        assert_eq!(ring.all_quads().len(), 30);
    }

    #[test]
    fn transition_validation_rejects_bypass() {
        let mut ring = Ring::new();
        ring.state = RingState::Ring;

        // Valid: FU → MU
        assert!(ring.validate_transition(UnitId::FU, UnitId::MU).is_ok());

        // Invalid: FU → CU (bypass MU — violates Invariant 5)
        assert!(ring.validate_transition(UnitId::FU, UnitId::CU).is_err());

        // Invalid: FU → FU (self-loop)
        assert!(ring.validate_transition(UnitId::FU, UnitId::FU).is_err());
    }

    #[test]
    fn uninitialized_ring_rejects_transitions() {
        let ring = Ring::new();
        assert!(ring.validate_transition(UnitId::FU, UnitId::MU).is_err());
    }
}