haematite-cli 0.31.0

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
//! Who made a request: its author row (ACCESS-003 R4).
//!
//! A row records who made a change, never what it changed: the request's id,
//! its caller (with the person responsible when an agent made it), and the
//! versions it was asked at and written at. The receipt and the change's own
//! rows say what changed, so a row's size never grows with the change: a
//! delete of every id a batch admits writes the same row as a delete of one.

use serde::{Deserialize, Serialize};

use super::{Caller, ResolvedMutation, identifier};
use crate::serve::protocol::Version;
use crate::serve::storage::Failure;

#[path = "attribution_key.rs"]
mod keys;
pub use keys::{decode_key, key, version_prefix};

pub const TAG: &[u8] = b"a\0";
pub const MAX_ROW_BYTES: usize = 64 * 1024;
/// Format 2 holds no change. Format 1, written only by the unreleased
/// haematite-cli of commit 7099cab7, also held what the change named; its
/// rows are still read, and that part is not kept.
const FORMAT: u32 = 2;
const FORMAT_WITH_CHANGE: u32 = 1;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ActorKind {
    Person,
    Agent,
}

/// A stored author is mandatory and never resolved from a current session.
#[derive(Debug, Serialize)]
pub struct Row {
    format: u32,
    version: Version,
    request_id: String,
    caller: Caller,
    origin: Version,
}

#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Wire {
    format: u32,
    version: Version,
    request_id: String,
    caller: Caller,
    origin: Version,
    /// Format 1's description of the change, read and left.
    #[serde(default)]
    change: Option<serde::de::IgnoredAny>,
}

#[derive(Deserialize)]
struct Header {
    format: Option<u64>,
}

impl Row {
    /// The author row of a resolved request, written at `written_at`.
    ///
    /// # Errors
    /// Returns `invalid_identifier` for an invalid request id.
    pub fn for_request(request: &ResolvedMutation, written_at: Version) -> Result<Self, Failure> {
        Self::by(
            request.request_id(),
            request.caller(),
            request.origin(),
            written_at,
        )
    }

    /// The author row of request `request_id`, asked at `origin` and written
    /// at `written_at`, made by `caller`: the author the store itself
    /// established for the request, never one the request named
    /// (ACCESS-003 R4). It holds nothing of the change.
    ///
    /// # Errors
    /// Returns `invalid_identifier` for an invalid request id.
    pub fn by(
        request_id: &str,
        caller: &Caller,
        origin: Version,
        written_at: Version,
    ) -> Result<Self, Failure> {
        identifier(request_id)?;
        Ok(Self {
            format: FORMAT,
            version: written_at,
            request_id: request_id.to_owned(),
            caller: caller.clone(),
            origin,
        })
    }

    /// Who made this request, as history answers it beside its version:
    /// the request's id and its caller.
    #[must_use]
    pub fn author(&self) -> serde_json::Value {
        serde_json::json!({"request_id": self.request_id, "caller": self.caller})
    }

    /// Encode this row's version and request identity.
    ///
    /// # Errors
    /// Returns `invalid_identifier` for an invalid stored request id.
    pub fn key(&self) -> Result<Vec<u8>, Failure> {
        key(self.version, &self.request_id)
    }

    /// Serialize one attribution row within the stored row bound.
    ///
    /// # Errors
    /// Returns `storage_unavailable` on serialization failure or the named row-size refusal when the row is too large.
    pub fn encode(&self) -> Result<Vec<u8>, Failure> {
        let bytes = serde_json::to_vec(self).map_err(Failure::storage)?;
        row_bound(&bytes)?;
        Ok(bytes)
    }

    /// Decode a bounded row and verify that its identity matches its key.
    ///
    /// # Errors
    /// Refuses oversized or malformed rows, unsupported formats, invalid keys
    /// and mismatched identities. A format 1 row must carry its change, which
    /// is read and left; a format 2 row must not.
    pub fn decode(key: &[u8], value: &[u8]) -> Result<Self, Failure> {
        row_bound(value)?;
        let (version, request_id) = decode_key(key)?;
        let header: Header = serde_json::from_slice(value).map_err(corrupt)?;
        if header.format != Some(u64::from(FORMAT))
            && header.format != Some(u64::from(FORMAT_WITH_CHANGE))
        {
            return Err(Failure::new(
                "change_record_format_unsupported",
                "attribution row is not format 1 or 2",
            ));
        }
        let wire: Wire = serde_json::from_slice(value).map_err(corrupt)?;
        if wire.change.is_some() != (wire.format == FORMAT_WITH_CHANGE) {
            return Err(corrupt(
                "a format 1 row names its change and a format 2 row does not",
            ));
        }
        if wire.version != version || wire.request_id != request_id {
            return Err(Failure::new(
                "change_record_key_mismatch",
                "attribution version or request id differs from its key",
            ));
        }
        Ok(Self {
            format: FORMAT,
            version: wire.version,
            request_id: wire.request_id,
            caller: wire.caller,
            origin: wire.origin,
        })
    }

    #[must_use]
    pub const fn version(&self) -> Version {
        self.version
    }
    #[must_use]
    pub fn request_id(&self) -> &str {
        &self.request_id
    }
    #[must_use]
    pub const fn caller(&self) -> &Caller {
        &self.caller
    }
    #[must_use]
    pub const fn origin(&self) -> Version {
        self.origin
    }
    #[must_use]
    pub const fn actor_kind(&self) -> ActorKind {
        if self.caller.responsible.is_some() {
            ActorKind::Agent
        } else {
            ActorKind::Person
        }
    }
}

fn corrupt(error: impl std::fmt::Display) -> Failure {
    Failure::new("change_record_corrupt", error.to_string())
}

fn row_bound(bytes: &[u8]) -> Result<(), Failure> {
    if bytes.len() > MAX_ROW_BYTES {
        return Err(Failure::new(
            "change_record_too_large",
            "attribution row exceeds 65536 bytes",
        ));
    }
    Ok(())
}

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

#[cfg(test)]
#[path = "attribution_size_tests.rs"]
mod size_tests;