haematite-cli 0.33.0

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
//! An author row records who made a change, never what it changed: its size
//! is the same whatever the size of the change (ACCESS-003 R4, second round).

use std::error::Error;

use serde_json::{Value, json};

use super::Row;
use crate::serve::contracts::Caller;
use crate::serve::protocol::{Change, Mutation, Version};
use crate::serve::storage::author_write;

type TestResult = Result<(), Box<dyn Error>>;

/// The largest batch the test asks for: `max_page × 64` ids at a page of
/// 1,000, past every page bound a served test or Tom's walk uses.
const MOST: usize = 1_000 * 64;

/// Each row must stay under one kibibyte.
const KIBIBYTE: usize = 1_024;

const fn version(sequence: u64) -> Version {
    Version {
        generation: 7,
        sequence,
    }
}

fn id(index: usize) -> String {
    format!("record-{index:05}")
}

fn node(index: usize) -> Value {
    json!({"collection": "charts", "document_id": id(index)})
}

/// A dense vector field declaration, as `vector.declare` takes one.
fn declaration() -> Value {
    json!({"kind": "dense", "source": "body", "model": {"name": "local-model", "revision": "v1",
        "manifest_sha256": "a".repeat(64)}, "dimensions": 2, "distance": "cosine"})
}

fn long(text: &str) -> String {
    text.repeat(512 / text.len())
}

/// Each change kind at its smallest and at a size far beyond it.
fn cases() -> Vec<(&'static str, Value, Value)> {
    let mut cases = batch_cases();
    cases.extend(single_cases());
    cases
}

/// The batch kinds, at one entry and at the most ids a batch admits.
fn batch_cases() -> Vec<(&'static str, Value, Value)> {
    let ids = |count: usize| (0..count).map(id).collect::<Vec<_>>();
    let documents = |count: usize| {
        (0..count)
            .map(|index| json!({"document_id": id(index), "document": {"n": index}}))
            .collect::<Vec<_>>()
    };
    let edges = |count: usize| {
        (0..count)
            .map(|index| json!({"relation": "link", "from": node(index), "to": node(index + 1), "properties": {}}))
            .collect::<Vec<_>>()
    };
    let vectors = |count: usize| {
        (0..count)
            .map(|index| json!({"collection": "charts", "document_id": id(index), "field": "embedding", "vector": [1.0]}))
            .collect::<Vec<_>>()
    };
    vec![
        (
            "documents.delete_many",
            json!({"collection": "charts", "document_ids": ids(1)}),
            json!({"collection": "charts", "document_ids": ids(MOST)}),
        ),
        (
            "documents.put_many",
            json!({"collection": "charts", "documents": documents(1)}),
            json!({"collection": "charts", "documents": documents(MOST)}),
        ),
        (
            "edges.put_many",
            json!({"edges": edges(1)}),
            json!({"edges": edges(MOST)}),
        ),
        (
            "vectors.put_many",
            json!({"vectors": vectors(1)}),
            json!({"vectors": vectors(MOST)}),
        ),
    ]
}

/// The single kinds, at one-letter names and at 512-byte names.
fn single_cases() -> Vec<(&'static str, Value, Value)> {
    let big = long("x");
    vec![
        (
            "document.put",
            json!({"collection": "c", "document_id": "a", "document": {}}),
            json!({"collection": big, "document_id": big, "document": {"body": long("body")}}),
        ),
        (
            "document.delete",
            json!({"collection": "c", "document_id": "a"}),
            json!({"collection": big, "document_id": big}),
        ),
        (
            "document.append_prune",
            json!({"collection": "c", "scope": ["a"], "suffix": "s", "document": {}, "keep": 1}),
            json!({"collection": big, "scope": vec!["s".repeat(48); 8], "suffix": "x".repeat(48), "document": {}, "keep": 1}),
        ),
        (
            "collection.put",
            json!({"collection": "c", "schema": null}),
            json!({"collection": big, "schema": null}),
        ),
        (
            "edge.put",
            json!({"relation": "r", "from": {"collection": "c", "document_id": "a"},
                "to": {"collection": "c", "document_id": "b"}, "properties": {}}),
            json!({"relation": big, "from": {"collection": big, "document_id": big},
                "to": {"collection": big, "document_id": big}, "properties": {}}),
        ),
        (
            "edge.delete",
            json!({"relation": "r", "from": {"collection": "c", "document_id": "a"},
                "to": {"collection": "c", "document_id": "b"}}),
            json!({"relation": big, "from": {"collection": big, "document_id": big},
                "to": {"collection": big, "document_id": big}}),
        ),
        (
            "vector.put",
            json!({"collection": "c", "document_id": "a", "field": "f", "vector": [1.0]}),
            json!({"collection": big, "document_id": big, "field": big, "vector": [1.0]}),
        ),
        (
            "vector.delete",
            json!({"collection": "c", "document_id": "a", "field": "f"}),
            json!({"collection": big, "document_id": big, "field": big}),
        ),
        (
            "vector.declare",
            json!({"collection": "c", "field": "f", "declaration": declaration()}),
            json!({"collection": big, "field": big, "declaration": declaration()}),
        ),
        (
            "index.declare",
            json!({"collection": "c", "field": "f"}),
            json!({"collection": big, "field": big}),
        ),
        (
            "relation.put",
            json!({"relation": "r", "from_collection": "c", "to_collection": "c",
                "cardinality": "many_to_many"}),
            json!({"relation": big, "from_collection": big, "to_collection": big,
                "cardinality": "many_to_many"}),
        ),
        (
            "relation.delete",
            json!({"relation": "r"}),
            json!({"relation": big}),
        ),
        (
            "branch.fork",
            json!({"name": "w", "from": version(1)}),
            json!({"name": big, "from": version(1)}),
        ),
        ("branch.discard", json!({"name": "w"}), json!({"name": big})),
        (
            "branch.merge",
            json!({"from": "w", "reviewed": version(1)}),
            json!({"from": big, "reviewed": version(1)}),
        ),
    ]
}

/// Every change kind, by an exhaustive match, so a kind added to `Change`
/// does not compile here until it has a case above.
const fn kind_of(change: &Change) -> &'static str {
    match change {
        Change::Collection(_) => "collection.put",
        Change::Put(_) => "document.put",
        Change::AppendPrune(_) => "document.append_prune",
        Change::Delete(_) => "document.delete",
        Change::DeleteMany(_) => "documents.delete_many",
        Change::Fork(_) => "branch.fork",
        Change::Discard(_) => "branch.discard",
        Change::Merge(_) => "branch.merge",
        Change::EdgePut(_) => "edge.put",
        Change::EdgeDelete(_) => "edge.delete",
        Change::IndexDeclare(_) => "index.declare",
        Change::VectorDeclare(_) => "vector.declare",
        Change::VectorPut(_) => "vector.put",
        Change::VectorPutMany(_) => "vectors.put_many",
        Change::VectorDelete(_) => "vector.delete",
        Change::PutMany(_) => "documents.put_many",
        Change::EdgePutMany(_) => "edges.put_many",
        Change::RelationPut(_) => "relation.put",
        Change::RelationDelete(_) => "relation.delete",
    }
}

/// The match above has 19 arms; the cases name each of them once.
const KINDS: usize = 19;

/// The encoded author row the store writes for `value` as change `kind`, as
/// an agent answering to a person made it, or why it was refused.
fn size(kind: &str, value: &Value) -> Result<usize, String> {
    let change: Change = serde_json::from_value(json!({"kind": kind, "value": value}))
        .map_err(|error| format!("{kind}: the change does not parse: {error}"))?;
    if kind_of(&change) != kind {
        return Err(format!("{kind} parsed as {}", kind_of(&change)));
    }
    let caller = Caller::new(
        "https://lys.example",
        "agent-7",
        Some("clinician".to_owned()),
    )
    .map_err(|failure| failure.message)?;
    let request = Mutation {
        request_id: "request-1".to_owned(),
        branch: "main".to_owned(),
        expected: version(1),
        change,
    };
    author_write(&request, &caller, version(2))
        .map(|(_, bytes)| bytes.len())
        .map_err(|failure| format!("{kind}: {}: {}", failure.code, failure.message))
}

/// Fails while any change kind's author row is refused, reaches a kibibyte,
/// or grows with the change: every kind that does is named.
#[test]
fn no_author_row_grows_with_its_change() {
    let mut grown = Vec::new();
    let mut named = std::collections::BTreeSet::new();
    for (kind, small, large) in cases() {
        named.insert(kind);
        match (size(kind, &small), size(kind, &large)) {
            (Ok(small), Ok(large)) if small == large && large < KIBIBYTE => {}
            (small, large) => grown.push(format!("{kind}: small {small:?}, large {large:?}")),
        }
    }
    assert_eq!(named.len(), KINDS, "{named:?}");
    assert!(
        grown.is_empty(),
        "author rows that grow with their change:\n{}",
        grown.join("\n")
    );
}

/// Fails while an author row holds anything but who made the request: the
/// request's id, its caller, and the versions it was asked at and written at.
#[test]
fn an_author_row_holds_who_and_never_what() -> TestResult {
    let change: Change = serde_json::from_value(json!({"kind": "documents.delete_many",
        "value": {"collection": "charts", "document_ids": ["secret-id-1", "secret-id-2"]}}))?;
    let request = Mutation {
        request_id: "delete-1".to_owned(),
        branch: "main".to_owned(),
        expected: version(1),
        change,
    };
    let (key, bytes) = author_write(&request, &Caller::local("tom")?, version(2))?;
    let stored: Value = serde_json::from_slice(&bytes)?;
    let mut members: Vec<_> = stored
        .as_object()
        .ok_or("not an object")?
        .keys()
        .cloned()
        .collect();
    members.sort();
    assert_eq!(
        members,
        ["caller", "format", "origin", "request_id", "version"]
    );
    assert!(!stored.to_string().contains("secret-id"), "{stored}");
    let decoded = Row::decode(&key, &bytes)?;
    assert_eq!(
        decoded.author(),
        json!({"request_id": "delete-1", "caller": {"issuer": "local", "subject": "tom"}})
    );
    Ok(())
}