thermogram 1.0.0

Plastic memory capsule with 4-temperature tensor states (hot/warm/cool/cold), bidirectional transitions, and hash-chained auditability
Documentation
//! Delta - Represents a change to the Thermogram state
//!
//! Deltas are append-only records of state changes. They form the "dirty" state
//! before consolidation.
//!
//! ## Signal-Native Strength
//!
//! All deltas use `Signal` for strength values. Signal encodes polarity (+/-/0)
//! and magnitude (0-255) in 2 bytes, replacing the old f32 + optional TernaryWeight
//! dual representation.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use ternary_signal::Signal;

/// A delta represents a single change to the Thermogram state
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Delta {
    /// Unique ID for this delta
    pub id: String,

    /// Type of operation
    pub delta_type: DeltaType,

    /// Key being modified
    pub key: String,

    /// New value (vector of Signals)
    pub value: Vec<Signal>,

    /// Metadata about the change
    pub metadata: DeltaMetadata,

    /// Hash of previous delta (forms chain)
    pub prev_hash: Option<String>,

    /// Hash of this delta
    pub hash: String,
}

/// Types of delta operations
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeltaType {
    /// Create new key
    Create,

    /// Update existing key
    Update,

    /// Delete key (tombstone)
    Delete,

    /// Merge with existing value (for incremental updates)
    Merge,
}

/// Metadata attached to each delta
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeltaMetadata {
    /// When this delta was created
    pub timestamp: DateTime<Utc>,

    /// Source of the delta (e.g., "llm_mining", "user_edit")
    pub source: String,

    /// Strength of this update — Signal encodes polarity + magnitude
    pub strength: Signal,

    /// Optional: Number of observations contributing to this update
    pub observation_count: Option<usize>,

    /// Custom metadata (raw bytes, None by default)
    #[serde(default)]
    pub custom: Option<Vec<u8>>,
}

impl Delta {
    /// Create a new delta
    pub fn new(
        delta_type: DeltaType,
        key: String,
        value: Vec<Signal>,
        source: String,
        strength: Signal,
        prev_hash: Option<String>,
    ) -> Self {
        let metadata = DeltaMetadata {
            timestamp: Utc::now(),
            source,
            strength,
            observation_count: None,
            custom: None,
        };

        let mut delta = Self {
            id: uuid::Uuid::new_v4().to_string(),
            delta_type,
            key,
            value,
            metadata,
            prev_hash,
            hash: String::new(), // Computed below
        };

        delta.hash = delta.compute_hash();
        delta
    }

    /// Create a CREATE delta (default strength = max positive)
    pub fn create(key: impl Into<String>, value: Vec<Signal>, source: impl Into<String>) -> Self {
        Self::new(
            DeltaType::Create,
            key.into(),
            value,
            source.into(),
            Signal::MAX_POSITIVE,
            None,
        )
    }

    /// Create an UPDATE delta
    pub fn update(
        key: impl Into<String>,
        value: Vec<Signal>,
        source: impl Into<String>,
        strength: Signal,
        prev_hash: Option<String>,
    ) -> Self {
        Self::new(
            DeltaType::Update,
            key.into(),
            value,
            source.into(),
            strength,
            prev_hash,
        )
    }

    /// Create a DELETE delta
    pub fn delete(key: impl Into<String>, source: impl Into<String>, prev_hash: Option<String>) -> Self {
        Self::new(
            DeltaType::Delete,
            key.into(),
            vec![],
            source.into(),
            Signal::MAX_POSITIVE,
            prev_hash,
        )
    }

    /// Create a MERGE delta (incremental update)
    pub fn merge(
        key: impl Into<String>,
        value: Vec<Signal>,
        source: impl Into<String>,
        strength: Signal,
        prev_hash: Option<String>,
    ) -> Self {
        Self::new(
            DeltaType::Merge,
            key.into(),
            value,
            source.into(),
            strength,
            prev_hash,
        )
    }

    /// Compute hash of this delta
    pub fn compute_hash(&self) -> String {
        let mut hasher = Sha256::new();

        // Hash all fields except the hash itself
        hasher.update(self.id.as_bytes());
        hasher.update(&[self.delta_type as u8]);
        hasher.update(self.key.as_bytes());

        // Hash value signals (2 bytes each: polarity + magnitude)
        for signal in &self.value {
            hasher.update(&[signal.polarity as u8, signal.magnitude]);
        }

        hasher.update(self.metadata.timestamp.to_rfc3339().as_bytes());
        hasher.update(self.metadata.source.as_bytes());

        // Hash strength as 2 bytes
        hasher.update(&[self.metadata.strength.polarity as u8, self.metadata.strength.magnitude]);

        if let Some(ref prev) = self.prev_hash {
            hasher.update(prev.as_bytes());
        }

        format!("{:x}", hasher.finalize())
    }

    /// Verify this delta's hash
    pub fn verify_hash(&self) -> bool {
        self.hash == self.compute_hash()
    }

    /// Verify this delta follows from the previous one
    pub fn verify_chain(&self, prev: &Delta) -> bool {
        match &self.prev_hash {
            Some(prev_hash) => prev_hash == &prev.hash,
            None => false,
        }
    }
}

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

    #[test]
    fn test_delta_creation() {
        let value = vec![Signal::positive(128), Signal::negative(64)];
        let delta = Delta::create("test_key", value.clone(), "test_source");

        assert_eq!(delta.delta_type, DeltaType::Create);
        assert_eq!(delta.key, "test_key");
        assert_eq!(delta.value, value);
        assert!(delta.verify_hash());
    }

    #[test]
    fn test_delta_chain() {
        let delta1 = Delta::create("key", vec![Signal::positive(100)], "source");
        let delta2 = Delta::update(
            "key",
            vec![Signal::positive(200)],
            "source",
            Signal::positive(204), // ~0.8
            Some(delta1.hash.clone()),
        );

        assert!(delta2.verify_chain(&delta1));
    }

    #[test]
    fn test_hash_stability() {
        let delta = Delta::create("key", vec![Signal::positive(100)], "source");
        let hash1 = delta.hash.clone();
        let hash2 = delta.compute_hash();

        assert_eq!(hash1, hash2);
    }
}