kcode-k1-kmap-format 0.2.0

Durable Kmap domain invariants and versioned binary formats
Documentation
use kcode_k1_transaction_id::TxId;
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, error::Error as StdError, fmt};

pub const FORMAT_VERSION: u8 = 2;
pub const EWA_HALF_LIFE_MASS: f64 = 30.0;
pub const PRUNE_EPSILON: f64 = 0.1;
pub const MAX_NAVIGATION_CONNECTIONS: usize = 12;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct KmapError(pub String);

impl fmt::Display for KmapError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.0)
    }
}

impl StdError for KmapError {}
pub type Result<T> = std::result::Result<T, KmapError>;

fn error(message: &str) -> KmapError {
    KmapError(message.to_owned())
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct NodeId(pub [u8; 12]);

impl From<TxId> for NodeId {
    fn from(value: TxId) -> Self {
        Self(*value.as_bytes())
    }
}

impl From<NodeId> for TxId {
    fn from(value: NodeId) -> Self {
        Self::from_bytes(value.0)
    }
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum ConnectionTier {
    Navigation,
    Automated,
}

#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
pub struct Weight {
    pub value: f64,
    pub mass: f64,
}

impl Weight {
    pub fn new(value: f64, mass: f64) -> Result<Self> {
        validate_value_mass(value, mass)?;
        Ok(Self { value, mass })
    }

    pub const fn initial() -> Self {
        Self {
            value: 1.0,
            mass: 3.0,
        }
    }

    pub fn update(self, value: f64, mass: f64) -> Result<Option<Self>> {
        validate_value_mass(self.value, self.mass)?;
        validate_value_mass(value, mass)?;
        let aged_mass = self.mass * (-mass / EWA_HALF_LIFE_MASS).exp2();
        let new_mass = aged_mass + mass;
        if !new_mass.is_finite() || new_mass <= 0.0 {
            return Err(error("updated mass is not representable"));
        }
        let new_value = (value - self.value)
            .mul_add(mass / new_mass, self.value)
            .clamp(0.0, 1.0);
        let updated = Self::new(new_value, new_mass)?;
        Ok((updated.value >= PRUNE_EPSILON).then_some(updated))
    }
}

#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Connection {
    pub target: NodeId,
    pub tier: ConnectionTier,
    pub weight: Weight,
}

impl Connection {
    pub fn new(target: NodeId, tier: ConnectionTier) -> Self {
        Self {
            target,
            tier,
            weight: Weight::initial(),
        }
    }
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ConnectionSpec {
    pub target: NodeId,
    pub tier: ConnectionTier,
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum MeasurementImportance {
    Critical,
    NonCritical,
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ConnectionMeasurement {
    pub source: NodeId,
    pub target: NodeId,
    pub useful: bool,
    pub importance: MeasurementImportance,
}

impl ConnectionMeasurement {
    pub const fn new(
        source: NodeId,
        target: NodeId,
        useful: bool,
        importance: MeasurementImportance,
    ) -> Self {
        Self {
            source,
            target,
            useful,
            importance,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum MeasurementOutcome {
    Updated,
    Pruned,
    Absent,
}

#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct Node {
    pub title: String,
    pub navigation_hint: String,
    pub narrative: String,
    pub connections: Vec<Connection>,
}

impl Node {
    pub fn new(
        title: impl Into<String>,
        navigation_hint: impl Into<String>,
        narrative: impl Into<String>,
        connections: Vec<Connection>,
    ) -> Result<Self> {
        let node = Self {
            title: title.into(),
            navigation_hint: navigation_hint.into(),
            narrative: narrative.into(),
            connections,
        };
        node.validate()?;
        Ok(node)
    }

    pub fn from_specs(
        title: impl Into<String>,
        navigation_hint: impl Into<String>,
        narrative: impl Into<String>,
        specs: Vec<ConnectionSpec>,
    ) -> Result<Self> {
        validate_specs(&specs)?;
        Self::new(
            title,
            navigation_hint,
            narrative,
            specs
                .into_iter()
                .map(|spec| Connection::new(spec.target, spec.tier))
                .collect(),
        )
    }

    pub fn validate(&self) -> Result<()> {
        ensure_unique(self.connections.iter().map(|connection| connection.target))?;
        for connection in &self.connections {
            validate_value_mass(connection.weight.value, connection.weight.mass)?;
        }
        validate_navigation(self.connections.iter().map(|connection| connection.tier))
    }

    pub fn apply_connection_updates(&mut self, updates: &[ConnectionSpec]) -> Result<()> {
        ensure_unique(updates.iter().map(|update| update.target))?;
        let mut connections = self.connections.clone();
        for update in updates {
            if let Some(connection) = connections
                .iter_mut()
                .find(|connection| connection.target == update.target)
            {
                connection.tier = update.tier;
            } else {
                connections.push(Connection::new(update.target, update.tier));
            }
        }
        let replacement = Self::new(
            self.title.clone(),
            self.navigation_hint.clone(),
            self.narrative.clone(),
            connections,
        )?;
        self.connections = replacement.connections;
        Ok(())
    }

    pub fn apply_measurement(
        &mut self,
        target: NodeId,
        useful: bool,
        importance: MeasurementImportance,
    ) -> Result<MeasurementOutcome> {
        let Some(index) = self
            .connections
            .iter()
            .position(|connection| connection.target == target)
        else {
            return Ok(MeasurementOutcome::Absent);
        };
        let value = if useful { 1.0 } else { 0.0 };
        let mass = match importance {
            MeasurementImportance::Critical => 3.0,
            MeasurementImportance::NonCritical => 1.0,
        };
        match self.connections[index].weight.update(value, mass)? {
            Some(weight) => {
                self.connections[index].weight = weight;
                Ok(MeasurementOutcome::Updated)
            }
            None => {
                self.connections.remove(index);
                Ok(MeasurementOutcome::Pruned)
            }
        }
    }

    pub fn encode(&self) -> Result<Vec<u8>> {
        self.validate()?;
        encode(self)
    }

    pub fn decode(bytes: &[u8]) -> Result<Self> {
        let value: Self = decode(bytes)?;
        value.validate()?;
        Ok(value)
    }
}

#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum KmapAction {
    CreateNode {
        title: String,
        navigation_hint: String,
        narrative: String,
        connections: Vec<ConnectionSpec>,
    },
    UpdateNode {
        node_id: NodeId,
        title: Option<String>,
        navigation_hint: Option<String>,
        narrative: Option<String>,
        connection_updates: Vec<ConnectionSpec>,
    },
    ApplyMeasurements {
        measurements: Vec<ConnectionMeasurement>,
    },
}

impl KmapAction {
    pub fn create_node(
        title: impl Into<String>,
        navigation_hint: impl Into<String>,
        narrative: impl Into<String>,
        connections: Vec<ConnectionSpec>,
    ) -> Result<Self> {
        let value = Self::CreateNode {
            title: title.into(),
            navigation_hint: navigation_hint.into(),
            narrative: narrative.into(),
            connections,
        };
        value.validate()?;
        Ok(value)
    }

    pub fn update_node(
        node_id: NodeId,
        title: Option<String>,
        navigation_hint: Option<String>,
        narrative: Option<String>,
        connection_updates: Vec<ConnectionSpec>,
    ) -> Result<Self> {
        let value = Self::UpdateNode {
            node_id,
            title,
            navigation_hint,
            narrative,
            connection_updates,
        };
        value.validate()?;
        Ok(value)
    }

    pub fn apply_measurements(measurements: Vec<ConnectionMeasurement>) -> Result<Self> {
        let value = Self::ApplyMeasurements { measurements };
        value.validate()?;
        Ok(value)
    }

    pub fn validate(&self) -> Result<()> {
        match self {
            Self::CreateNode { connections, .. } => validate_specs(connections),
            Self::UpdateNode {
                connection_updates, ..
            } => ensure_unique(connection_updates.iter().map(|update| update.target)),
            Self::ApplyMeasurements { .. } => Ok(()),
        }
    }

    pub fn encode(&self) -> Result<Vec<u8>> {
        self.validate()?;
        encode(self)
    }

    pub fn decode(bytes: &[u8]) -> Result<Self> {
        let value: Self = decode(bytes)?;
        value.validate()?;
        Ok(value)
    }
}

fn validate_value_mass(value: f64, mass: f64) -> Result<()> {
    if !value.is_finite() || !(0.0..=1.0).contains(&value) {
        return Err(error("value must be finite and in [0, 1]"));
    }
    if !mass.is_finite() || mass <= 0.0 {
        return Err(error("mass must be finite and positive"));
    }
    Ok(())
}

fn validate_specs(specs: &[ConnectionSpec]) -> Result<()> {
    ensure_unique(specs.iter().map(|spec| spec.target))?;
    validate_navigation(specs.iter().map(|spec| spec.tier))
}

fn ensure_unique(targets: impl IntoIterator<Item = NodeId>) -> Result<()> {
    let mut seen = HashSet::new();
    if targets.into_iter().all(|target| seen.insert(target)) {
        Ok(())
    } else {
        Err(error("duplicate connection target"))
    }
}

fn validate_navigation(tiers: impl IntoIterator<Item = ConnectionTier>) -> Result<()> {
    if tiers
        .into_iter()
        .filter(|tier| *tier == ConnectionTier::Navigation)
        .count()
        > MAX_NAVIGATION_CONNECTIONS
    {
        Err(error("too many Navigation connections"))
    } else {
        Ok(())
    }
}

fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>> {
    let mut bytes = vec![FORMAT_VERSION];
    bytes.extend(postcard::to_stdvec(value).map_err(|_| error("wire encoding failed"))?);
    Ok(bytes)
}

fn decode<T: for<'a> Deserialize<'a>>(bytes: &[u8]) -> Result<T> {
    let Some((&version, payload)) = bytes.split_first() else {
        return Err(error("empty wire value"));
    };
    if version != FORMAT_VERSION {
        return Err(error("unsupported wire version"));
    }
    let (value, trailing) =
        postcard::take_from_bytes(payload).map_err(|_| error("malformed wire payload"))?;
    if trailing.is_empty() {
        Ok(value)
    } else {
        Err(error("trailing wire bytes"))
    }
}