soul-base 0.1.0

Data contract primitives for the Soul platform (IDs, Subject, Scope, Consent, Envelope, ...).
Documentation
//! Ownership Level (L1-L4)
//!
//! Defines the four-level classification for events per MODEL-403/RULE-404.
//!
//! # Life Graph Center Principle
//!
//! - Default query lens: Single AI's life history only includes L1 + L2
//! - L3 system events cannot be injected into life trunk, only traceable via refs
//! - L4 views never enter fact layer

use serde::{Deserialize, Serialize};
use std::fmt;
#[cfg(feature = "schema")]
use schemars::JsonSchema;

/// Event Ownership Level (MODEL-403)
///
/// Every event must declare its ownership level. This classification determines:
/// - Which events appear in the life lens (default: L1 + L2)
/// - Which events can trigger strong consequences
/// - Actor type constraints per RULE-404
///
/// Per CommonEventBase.schema.json: serializes as "L1", "L2", "L3", "L4"
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub enum OwnershipLevel {
    /// L1 Actor-Life: AI life facts
    ///
    /// Events triggered/participated/directly related by the AIActor.
    /// Represents life journey: dialogue, decisions, tool interactions, memory, knowledge, self, collective behavior.
    ///
    /// - `actor_id` must be AIActor (or HumanActor for human events)
    /// - Included in default life lens
    #[serde(rename = "L1")]
    L1ActorLife,

    /// L2 Actor-Reflection: Public reality consequences on AI
    ///
    /// Public reality stack consequences on this AI: receipts, settlements, anchor results, verdicts, permission changes.
    /// Must reference corresponding system fact objects (receipt/proof/link/bundle).
    ///
    /// - `actor_id` must be AIActor
    /// - Must have `subject_actor_id` matching the affected AI
    /// - Included in default life lens
    #[serde(rename = "L2")]
    L2ActorReflection,

    /// L3 System-Node: System facts
    ///
    /// OSNodeActor/CoreNodeActor generated public facts: epoch, bundle, anchor job, map commit, conformance run.
    /// Does not enter single AI life line by default, can only be traced via refs.
    ///
    /// - `actor_id` must be OSNodeActor or CoreNodeActor
    /// - Must reference epoch_id
    /// - Not included in default life lens
    #[serde(rename = "L3")]
    L3SystemNode,

    /// L4 Views/Metrics: Derived indicators
    ///
    /// Recomputable, mutable granularity, used for sorting/insights/dashboards.
    /// **Never enters fact layer** - not stored in S1/S2.
    ///
    /// Note: Events should never have L4 ownership as L4 doesn't enter fact layer.
    #[serde(rename = "L4")]
    L4Views,
}

impl OwnershipLevel {
    /// Check if this level is included in the default life lens (L1 + L2)
    pub fn in_life_lens(&self) -> bool {
        matches!(
            self,
            OwnershipLevel::L1ActorLife | OwnershipLevel::L2ActorReflection
        )
    }

    /// Check if this level can enter the fact layer (L1, L2, L3)
    pub fn can_enter_fact_layer(&self) -> bool {
        !matches!(self, OwnershipLevel::L4Views)
    }

    /// Check if this level requires epoch_id reference
    pub fn requires_epoch(&self) -> bool {
        matches!(self, OwnershipLevel::L3SystemNode)
    }

    /// Check if this level requires system actor (OSNode/CoreNode)
    pub fn requires_system_actor(&self) -> bool {
        matches!(self, OwnershipLevel::L3SystemNode)
    }

    /// Get the numeric level (1-4)
    pub fn level(&self) -> u8 {
        match self {
            OwnershipLevel::L1ActorLife => 1,
            OwnershipLevel::L2ActorReflection => 2,
            OwnershipLevel::L3SystemNode => 3,
            OwnershipLevel::L4Views => 4,
        }
    }
}

impl fmt::Display for OwnershipLevel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            OwnershipLevel::L1ActorLife => write!(f, "L1-ActorLife"),
            OwnershipLevel::L2ActorReflection => write!(f, "L2-ActorReflection"),
            OwnershipLevel::L3SystemNode => write!(f, "L3-SystemNode"),
            OwnershipLevel::L4Views => write!(f, "L4-Views"),
        }
    }
}

impl Default for OwnershipLevel {
    fn default() -> Self {
        OwnershipLevel::L1ActorLife
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_life_lens_inclusion() {
        assert!(OwnershipLevel::L1ActorLife.in_life_lens());
        assert!(OwnershipLevel::L2ActorReflection.in_life_lens());
        assert!(!OwnershipLevel::L3SystemNode.in_life_lens());
        assert!(!OwnershipLevel::L4Views.in_life_lens());
    }

    #[test]
    fn test_fact_layer_entry() {
        assert!(OwnershipLevel::L1ActorLife.can_enter_fact_layer());
        assert!(OwnershipLevel::L2ActorReflection.can_enter_fact_layer());
        assert!(OwnershipLevel::L3SystemNode.can_enter_fact_layer());
        assert!(!OwnershipLevel::L4Views.can_enter_fact_layer());
    }

    #[test]
    fn test_level_numbers() {
        assert_eq!(OwnershipLevel::L1ActorLife.level(), 1);
        assert_eq!(OwnershipLevel::L2ActorReflection.level(), 2);
        assert_eq!(OwnershipLevel::L3SystemNode.level(), 3);
        assert_eq!(OwnershipLevel::L4Views.level(), 4);
    }

    #[test]
    fn test_serialization() {
        assert_eq!(
            serde_json::to_string(&OwnershipLevel::L1ActorLife).unwrap(),
            "\"L1\""
        );
        assert_eq!(
            serde_json::to_string(&OwnershipLevel::L2ActorReflection).unwrap(),
            "\"L2\""
        );
        assert_eq!(
            serde_json::to_string(&OwnershipLevel::L3SystemNode).unwrap(),
            "\"L3\""
        );
        assert_eq!(
            serde_json::to_string(&OwnershipLevel::L4Views).unwrap(),
            "\"L4\""
        );

        for level in [
            OwnershipLevel::L1ActorLife,
            OwnershipLevel::L2ActorReflection,
            OwnershipLevel::L3SystemNode,
            OwnershipLevel::L4Views,
        ] {
            let json = serde_json::to_string(&level).unwrap();
            let parsed: OwnershipLevel = serde_json::from_str(&json).unwrap();
            assert_eq!(parsed, level);
        }
    }
}