haematite-cli 0.21.8

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
//! Content identities bind owner choices to the captured comparison, not admission.

use haematite::tree::Hash;

use super::super::{
    Captured, Failure,
    embedded::{self, Field},
    identifier,
};

pub(in crate::serve::storage) struct Pins(Hash);

impl Pins {
    pub(in crate::serve::storage) fn new(
        origin: &Captured,
        target: &Captured,
        source: &Captured,
    ) -> Result<Self, Failure> {
        let mut bytes = b"haem.merge-choice-pins.v1\0".to_vec();
        for at in [origin, target, source] {
            append_bytes(&mut bytes, at.branch.as_bytes())?;
            bytes.extend_from_slice(&at.version.generation.to_le_bytes());
            bytes.extend_from_slice(&at.version.sequence.to_le_bytes());
            append_length(&mut bytes, at.shards.len())?;
            for shard in &at.shards {
                append_length(&mut bytes, shard.shard_id)?;
                // Kept captures reconstruct anchors; only read heads identify these versions.
                bytes.extend_from_slice(shard.head.as_bytes());
            }
        }
        Ok(Self(Hash::of(&bytes)))
    }

    pub(in crate::serve::storage) fn token(
        &self,
        key: &[u8],
        ancestor: Option<&[u8]>,
        target: Option<&[u8]>,
        source: Option<&[u8]>,
    ) -> Result<String, Failure> {
        let mut bytes = b"haem.merge-choice-owner.v1\0".to_vec();
        bytes.extend_from_slice(self.0.as_bytes());
        append_bytes(&mut bytes, key)?;
        for value in [ancestor, target, source] {
            match value {
                None => bytes.push(0),
                Some(value) => {
                    bytes.push(1);
                    append_length(&mut bytes, value.len())?;
                    bytes.extend_from_slice(Hash::of(value).as_bytes());
                }
            }
        }
        // The later coverage proof must recompute this identity from server-read bytes.
        Ok(format!("merge-choice-v1:{}", Hash::of(&bytes)))
    }
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub(in crate::serve::storage) enum EmbeddedSide {
    Source,
    Target,
}

pub(in crate::serve::storage) fn embedded_field(
    key: &[u8],
    ancestor: Option<&[u8]>,
    target: Option<&[u8]>,
    source: Option<&[u8]>,
) -> Result<Option<EmbeddedSide>, Failure> {
    let Some(name) = key.strip_prefix(b"w\0") else {
        return Ok(None);
    };
    let text = std::str::from_utf8(name).map_err(Failure::storage)?;
    let (collection, field) = text
        .split_once('\0')
        .ok_or_else(|| Failure::new("corrupt_key", "vector field key has invalid components"))?;
    identifier(collection)?;
    identifier(field)?;
    let read = |bytes: Option<&[u8]>| bytes.map(embedded::read_field).transpose();
    let ancestor = read(ancestor)?;
    let target = read(target)?;
    let source = read(source)?;
    let versions = [ancestor.as_ref(), target.as_ref(), source.as_ref()];
    if !versions
        .iter()
        .any(|field| matches!(field, Some(Field::Embedded(_))))
    {
        return Ok(None);
    }
    if versions
        .iter()
        .any(|field| matches!(field, Some(Field::Supplied { .. })))
    {
        return Err(Failure::new(
            "vector_field_supplied",
            "an embedded declaration cannot replace a supplied field",
        ));
    }
    let mut declared = None;
    for field in versions.into_iter().flatten() {
        if let Field::Embedded(current) = field {
            if declared.is_some_and(|previous| previous != current) {
                return Err(Failure::new(
                    "embed_model_change_not_available",
                    "different embedded declarations cannot be merged",
                ));
            }
            declared = Some(current);
        }
    }
    if target == source || ancestor == source {
        Ok(Some(EmbeddedSide::Target))
    } else if ancestor == target {
        Ok(Some(EmbeddedSide::Source))
    } else {
        Err(Failure::new(
            "embed_model_change_not_available",
            "embedded declaration has conflicting field versions",
        ))
    }
}

fn append_length(bytes: &mut Vec<u8>, length: usize) -> Result<(), Failure> {
    let length = u64::try_from(length).map_err(|_| {
        Failure::new(
            "choice_identity_length",
            "choice input exceeds its length field",
        )
    })?;
    bytes.extend_from_slice(&length.to_le_bytes());
    Ok(())
}

fn append_bytes(bytes: &mut Vec<u8>, value: &[u8]) -> Result<(), Failure> {
    append_length(bytes, value.len())?;
    bytes.extend_from_slice(value);
    Ok(())
}

#[cfg(test)]
#[path = "review_choice_tests.rs"]
mod tests;