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
//! The `UnitProcessor` trait: structural boundary contract at the body-problem level.
//!
//! ## Spec traceability
//! - Spec §14.3: technology-agnosticism — "The Server layer of any unit could be
//!   implemented as a rule engine, a lookup table, a neural network, or an independent
//!   inference model without violating any structural invariant."
//! - SAD §4.2: "The processor never sees the external key."
//! - OPUS §2 Quad / §11 body-problem — trait encodes stripped-Quad-in/out at type level.
//!
//! ## Architectural location
//!
//! `UnitProcessor` is structural — it encodes the body-problem invariant at the type
//! level. A processor receives a stripped Quad (Pointer = sentinel) and emits a new
//! Quad. It never sees the predecessor's external key. This is the boundary between
//! structural constants (the ring protocol) and implementation variables (what each
//! unit actually computes). The trait belongs in soma-som-core; its implementations belong
//! in the organ crates or application tier.
//!
//! ## Separation from RingProcessor
//!
//! [`soma_som_ring::RingProcessor`] is the engine's minimal processing
//! contract (see the ring crate). [`UnitProcessor`] in this crate is the
//! richer user-facing trait for implementing custom unit logic — the engine
//! does not know about it. Application code adapts between the two with a
//! blanket impl.
//!
//! [`soma_som_ring::RingProcessor`]: https://docs.rs/soma-som-ring/0.1/soma_som_ring/processor/trait.RingProcessor.html

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

/// Errors produced by unit processor operations.
#[derive(Debug, Clone, thiserror::Error)]
#[non_exhaustive]
pub enum UnitProcessorError {
    /// The processor received an empty Quad during an active cycle.
    #[error("Empty input Quad for {unit} at cycle {cycle_index}")]
    EmptyInput {
        /// The unit that rejected the empty Quad.
        unit: UnitId,
        /// The cycle index at which the error occurred.
        cycle_index: u64,
    },

    /// The processor's output Quad failed validation.
    #[error("Invalid output from {unit}: {reason}")]
    InvalidOutput {
        /// The unit that produced the invalid output.
        unit: UnitId,
        /// Human-readable reason for the validation failure.
        reason: String,
    },

    /// A processor-internal error (implementation-specific).
    #[error("Processor error in {unit}: {reason}")]
    ProcessorError {
        /// The unit in which the error occurred.
        unit: UnitId,
        /// Human-readable error description.
        reason: String,
    },
}

/// The trait that any unit implementation must satisfy.
///
/// This is the protocol boundary contract from the processing side.
/// The input Quad has its external key ALREADY STRIPPED by the boundary.
/// The processor never sees the predecessor's Pointer.
///
/// ## Contract
///
/// 1. The processor receives a well-formed Quad (Root, Pointer=sentinel, Tree).
/// 2. The processor produces a well-formed Quad (Root, Pointer, Tree).
/// 3. The output Pointer becomes the key that the boundary strips before delivery
///    to the successor. The processor has no control over what happens after.
/// 4. The processor MUST NOT attempt to reconstruct the stripped Pointer.
///    There is no mechanism to do so within the type system.
///
/// ## Lifecycle
///
/// - `process()` is called once per ring cycle for this unit (Server layer).
/// - `process_layer()` is called for horizontal presentation processing
///   (Client, Interface layers).
/// - The processor may maintain internal state across cycles.
/// - Genesis processing uses the same interface; the input will be the
///   seed-derived Quad (for FU) or the predecessor's genesis output.
pub trait UnitProcessor: Send + 'static {
    /// Process an incoming (stripped) Quad and produce an output Quad.
    ///
    /// This is the Server-layer processing path (vertical ring backbone).
    fn process(
        &mut self,
        unit_id: UnitId,
        cycle_index: u64,
        input: &Quad,
        data: &Quad,
    ) -> Result<Quad, UnitProcessorError>;

    /// Process a Quad for a specific SOM layer (horizontal presentation path).
    ///
    /// ## Default implementation
    ///
    /// The default creates a deterministic transform based on unit ID, cycle,
    /// and target layer. Processors that need custom layer behavior override this.
    fn process_layer(
        &mut self,
        unit_id: UnitId,
        cycle_index: u64,
        target_layer: Layer,
        server_output: &Quad,
    ) -> Result<Quad, UnitProcessorError> {
        let root = {
            let mut hasher = blake3::Hasher::new();
            hasher.update(&[unit_id as u8]);
            hasher.update(&[target_layer as u8]);
            hasher.update(&cycle_index.to_le_bytes());
            hasher.update(&server_output.root);
            *hasher.finalize().as_bytes()
        };

        let pointer = {
            let mut hasher = blake3::Hasher::new();
            hasher.update(&[unit_id as u8]);
            hasher.update(&[target_layer as u8]);
            hasher.update(b"layer_pointer");
            hasher.update(&cycle_index.to_le_bytes());
            *hasher.finalize().as_bytes()
        };

        let mut tree = server_output.tree.clone();
        tree.insert(
            "processor.layer".into(),
            format!("{target_layer:?}").into_bytes(),
        );
        tree.insert("processor.unit".into(), format!("{unit_id}").into_bytes());

        Ok(Quad::new(root, pointer, tree))
    }

    /// Externalize accumulated processor state for Data layer deposit.
    ///
    /// Default: `None` — no state to externalize (backward compatible).
    fn externalize_state(&self, _unit_id: UnitId) -> Option<Quad> {
        None
    }

    /// Human-readable name for this processor implementation.
    fn name(&self) -> &str;
}

#[cfg(feature = "test-support")]
pub use test_support::{EchoProcessor, FailingProcessor, StubProcessor};

#[cfg(feature = "test-support")]
mod test_support {
    use super::{Quad, UnitId, UnitProcessor, UnitProcessorError};

    /// Stub processor: deterministic, identity-aware, minimal.
    ///
    /// Satisfies Criterion 2 (distinct fingerprints per position) without
    /// implementing any unit-specific domain logic. Used in ring-level tests.
    #[derive(Debug, Clone)]
    pub struct StubProcessor {
        invocation_count: u64,
    }

    impl StubProcessor {
        /// Create a new stub processor.
        pub fn new() -> Self {
            Self { invocation_count: 0 }
        }

        /// Get the invocation count (for diagnostics).
        pub fn invocation_count(&self) -> u64 {
            self.invocation_count
        }
    }

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

    impl UnitProcessor for StubProcessor {
        fn process(
            &mut self,
            unit_id: UnitId,
            cycle_index: u64,
            input: &Quad,
            _data: &Quad,
        ) -> Result<Quad, UnitProcessorError> {
            self.invocation_count += 1;

            let root = {
                let mut hasher = blake3::Hasher::new();
                hasher.update(&[unit_id as u8]);
                hasher.update(&cycle_index.to_le_bytes());
                hasher.update(&input.root);
                *hasher.finalize().as_bytes()
            };

            let pointer = {
                let mut hasher = blake3::Hasher::new();
                hasher.update(&[unit_id as u8]);
                hasher.update(b"pointer");
                hasher.update(&cycle_index.to_le_bytes());
                *hasher.finalize().as_bytes()
            };

            let mut tree = input.tree.clone();
            tree.insert("processor.unit".into(), format!("{unit_id}").into_bytes());
            tree.insert("processor.cycle".into(), cycle_index.to_le_bytes().to_vec());
            tree.insert(
                "processor.input_root_hash".into(),
                blake3::hash(&input.root).as_bytes().to_vec(),
            );
            tree.insert("processor.name".into(), b"StubProcessor".to_vec());

            Ok(Quad::new(root, pointer, tree))
        }

        fn name(&self) -> &str {
            "StubProcessor"
        }
    }

    /// Echo processor: returns the input Quad unmodified.
    #[derive(Debug, Clone, Default)]
    pub struct EchoProcessor;

    impl EchoProcessor {
        /// Create a new echo processor.
        pub fn new() -> Self {
            Self
        }
    }

    impl UnitProcessor for EchoProcessor {
        fn process(
            &mut self,
            _unit_id: UnitId,
            _cycle_index: u64,
            input: &Quad,
            _data: &Quad,
        ) -> Result<Quad, UnitProcessorError> {
            Ok(input.clone())
        }

        fn name(&self) -> &str {
            "EchoProcessor"
        }
    }

    /// Failing processor: always returns an error.
    ///
    /// Used for Criterion 1 (ring integrity) and fault injection testing.
    #[derive(Debug, Clone)]
    pub struct FailingProcessor {
        message: String,
    }

    impl FailingProcessor {
        /// Create a new failing processor with the given error message.
        pub fn new(message: impl Into<String>) -> Self {
            Self { message: message.into() }
        }
    }

    impl UnitProcessor for FailingProcessor {
        fn process(
            &mut self,
            unit_id: UnitId,
            _cycle_index: u64,
            _input: &Quad,
            _data: &Quad,
        ) -> Result<Quad, UnitProcessorError> {
            Err(UnitProcessorError::ProcessorError {
                unit: unit_id,
                reason: self.message.clone(),
            })
        }

        fn name(&self) -> &str {
            "FailingProcessor"
        }
    }

    #[cfg(test)]
    mod tests {
        use super::*;
        use crate::quad::Tree as QuadTree;
        use crate::types::Layer;

        fn make_input_quad() -> Quad {
            let mut tree = QuadTree::new();
            tree.insert("input.data".into(), vec![1, 2, 3]);
            Quad::from_strings("input_root", "stripped_sentinel", tree)
        }

        #[test]
        fn stub_produces_non_empty_output() {
            let mut proc = StubProcessor::new();
            let input = make_input_quad();
            let output = proc.process(UnitId::FU, 1, &input, &Quad::default()).unwrap();
            assert_ne!(output.root, [0u8; 32]);
            assert_ne!(output.pointer, [0u8; 32]);
            assert!(!output.tree.is_empty());
        }

        #[test]
        fn stub_output_depends_on_unit_id() {
            let input = make_input_quad();
            let out_fu = StubProcessor::new().process(UnitId::FU, 1, &input, &Quad::default()).unwrap();
            let out_mu = StubProcessor::new().process(UnitId::MU, 1, &input, &Quad::default()).unwrap();
            assert_ne!(out_fu.root, out_mu.root);
            assert_ne!(out_fu.pointer, out_mu.pointer);
        }

        #[test]
        fn stub_output_depends_on_cycle_index() {
            let input = make_input_quad();
            let out_c1 = StubProcessor::new().process(UnitId::FU, 1, &input, &Quad::default()).unwrap();
            let out_c2 = StubProcessor::new().process(UnitId::FU, 2, &input, &Quad::default()).unwrap();
            assert_ne!(out_c1.root, out_c2.root);
        }

        #[test]
        fn stub_pointer_does_not_depend_on_input_pointer() {
            let input_a = Quad::new([1u8; 32], [0u8; 32], QuadTree::new());
            let input_b = Quad::new([1u8; 32], [99u8; 32], QuadTree::new());
            let out_a = StubProcessor::new().process(UnitId::FU, 1, &input_a, &Quad::default()).unwrap();
            let out_b = StubProcessor::new().process(UnitId::FU, 1, &input_b, &Quad::default()).unwrap();
            assert_eq!(out_a.pointer, out_b.pointer);
        }

        #[test]
        fn echo_returns_input_unchanged() {
            let mut proc = EchoProcessor::new();
            let input = make_input_quad();
            let output = proc.process(UnitId::FU, 1, &input, &Quad::default()).unwrap();
            assert_eq!(output, input);
        }

        #[test]
        fn failing_processor_always_fails() {
            let mut proc = FailingProcessor::new("intentional failure");
            let input = make_input_quad();
            let result = proc.process(UnitId::FU, 1, &input, &Quad::default());
            assert!(result.is_err());
        }

        #[test]
        fn processor_is_send() {
            fn assert_send<T: Send>() {}
            assert_send::<StubProcessor>();
            assert_send::<EchoProcessor>();
            assert_send::<FailingProcessor>();
        }

        #[test]
        fn default_process_layer_produces_output() {
            let mut proc = StubProcessor::new();
            let server_output = proc.process(UnitId::FU, 1, &make_input_quad(), &Quad::default()).unwrap();
            let client_out = proc.process_layer(UnitId::FU, 1, Layer::Client, &server_output).unwrap();
            assert_ne!(client_out.root, [0u8; 32]);
        }

        #[test]
        fn unit_processor_error_display() {
            let e = UnitProcessorError::ProcessorError {
                unit: UnitId::FU,
                reason: "test".into(),
            };
            assert!(e.to_string().contains("FU"));
        }
    }
}