haematite-cli 0.21.7

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
use std::collections::BTreeSet;
use std::path::PathBuf;

use serde_json::Value;

use super::{Class, ENVELOPES, ROUTES, render};
use crate::serve::protocol::{Change, Operation};

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

fn design_file(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("../../docs/design/access")
        .join(name)
}

fn schema() -> Result<Value, Box<dyn std::error::Error>> {
    Ok(serde_json::from_slice(&std::fs::read(design_file(
        "schema.json",
    ))?)?)
}

/// Every served method and every change kind has exactly one row, and no row
/// names a route the protocol does not serve. A new variant fails here until
/// its row is written (ACCESS-001 R1: a route without its row fails the gate).
#[test]
fn every_served_route_has_exactly_one_row() {
    let served: BTreeSet<&str> = Operation::WIRE_NAMES
        .iter()
        .chain(Change::WIRE_NAMES)
        .copied()
        .filter(|wire| !ENVELOPES.contains(wire))
        .collect();
    let rows: Vec<&str> = ROUTES.iter().map(|route| route.wire).collect();
    let unique: BTreeSet<&str> = rows.iter().copied().collect();
    assert_eq!(rows.len(), unique.len(), "a route has two rows: {rows:?}");
    let missing: Vec<&&str> = served.difference(&unique).collect();
    let extra: Vec<&&str> = unique.difference(&served).collect();
    assert!(
        missing.is_empty(),
        "served routes without a row: {missing:?}"
    );
    assert!(
        extra.is_empty(),
        "rows for routes nothing serves: {extra:?}"
    );
    for envelope in ENVELOPES {
        assert!(
            Operation::WIRE_NAMES.contains(envelope),
            "the envelope {envelope} is not a served method"
        );
    }
}

/// Each row's action is declared on its resource kind in the registered
/// schema, so the matrix can never ask Lys about an action Lys does not know.
#[test]
fn every_row_names_an_action_its_kind_declares() -> TestResult {
    let schema = schema()?;
    for route in ROUTES {
        let kind = format!("haematite.{}", route.kind);
        let actions = schema["kinds"][&kind]["actions"]
            .as_array()
            .ok_or_else(|| format!("{}: the schema has no kind {kind}", route.wire))?;
        assert!(
            actions.iter().any(|action| action == route.action),
            "{}: {kind} does not declare {}",
            route.wire,
            route.action
        );
    }
    Ok(())
}

/// A route's class is its action's class in the registered schema: hot when
/// its kind names the action under `hot`, deliberate otherwise (D2). And the
/// class is fixed per action, never per request or per kind: read, query and
/// write are hot wherever they are declared; branch, merge, restore, copy and
/// serve are deliberate.
#[test]
fn every_row_has_the_class_its_schema_registers() -> TestResult {
    let schema = schema()?;
    let kinds = schema["kinds"]
        .as_object()
        .ok_or("the schema has no kinds")?;
    for (kind, body) in kinds {
        let hot: BTreeSet<&str> = body["hot"]
            .as_array()
            .map(|listed| listed.iter().filter_map(Value::as_str).collect())
            .unwrap_or_default();
        for action in body["actions"].as_array().ok_or("a kind with no actions")? {
            let action = action.as_str().ok_or("an action that is not a string")?;
            let fixed = matches!(action, "read" | "query" | "write");
            assert_eq!(
                hot.contains(action),
                fixed,
                "{kind} registers {action} in the wrong class"
            );
        }
    }
    for route in ROUTES {
        let kind = format!("haematite.{}", route.kind);
        let registered = schema["kinds"][&kind]["hot"]
            .as_array()
            .is_some_and(|hot| hot.iter().any(|action| action == route.action));
        let expected = if registered {
            Class::Hot
        } else {
            Class::Deliberate
        };
        assert_eq!(route.class, expected, "{} is misclassified", route.wire);
    }
    Ok(())
}

/// The observer can read and query and nothing else (S301: an observer is a
/// grant and can never write). A writer adds write and branch, and branch is
/// its only deliberate act, asked of Lys live each time: schema v4 (bb8fe95)
/// gave the writer branch because every haematite write starts on a branch,
/// so a writer who could not branch could not write. Merge stays the
/// administrator's alone.
#[test]
fn the_observer_never_writes_and_the_writers_only_deliberate_act_is_branch() -> TestResult {
    let schema = schema()?;
    let relations = &schema["kinds"]["haematite.database"]["relations"];
    let carried = |role: &str| -> Result<BTreeSet<String>, String> {
        relations[role]
            .as_array()
            .ok_or_else(|| format!("no role {role}"))?
            .iter()
            .map(|action| {
                action
                    .as_str()
                    .map(str::to_owned)
                    .ok_or_else(|| format!("{role} carries a non-string action"))
            })
            .collect()
    };
    let words = |list: &[&str]| -> BTreeSet<String> {
        list.iter().map(|word| (*word).to_owned()).collect()
    };
    assert_eq!(carried("observer")?, words(&["query", "read"]));
    let writer = carried("writer")?;
    assert_eq!(writer, words(&["branch", "query", "read", "write"]));
    let writers_deliberate: BTreeSet<String> = ROUTES
        .iter()
        .filter(|route| route.class == Class::Deliberate && writer.contains(route.action))
        .map(|route| route.action.to_owned())
        .collect();
    assert_eq!(writers_deliberate, words(&["branch"]));
    assert!(!writer.contains("merge"), "a writer never merges");
    assert!(
        carried("administrator")?.contains("merge"),
        "the administrator merges"
    );
    let declared: BTreeSet<String> = schema["kinds"]["haematite.database"]["actions"]
        .as_array()
        .ok_or("the database kind declares no actions")?
        .iter()
        .filter_map(|action| action.as_str().map(str::to_owned))
        .collect();
    assert_eq!(carried("administrator")?, declared);
    Ok(())
}

/// The committed MATRIX.md is the rendering of the rows, byte for byte.
#[test]
fn the_committed_matrix_is_the_rendered_rows() -> TestResult {
    let committed = std::fs::read_to_string(design_file("MATRIX.md"))?;
    assert_eq!(
        committed,
        render(),
        "docs/design/access/MATRIX.md differs from the route rows; render it again"
    );
    Ok(())
}

/// A review computes and changes nothing, so the routes that serve it are
/// reads (Waffles' ruling, 12 Oct 2026, on Hermes' finding that a writer could
/// not review an import branch): a writer reviews a branch, and no route a
/// writer's actions reach merges.
#[test]
fn a_writer_reviews_an_import_branch_and_never_merges() -> TestResult {
    let schema = schema()?;
    let writer: BTreeSet<&str> = schema["kinds"]["haematite.database"]["relations"]["writer"]
        .as_array()
        .ok_or("no writer role")?
        .iter()
        .filter_map(Value::as_str)
        .collect();
    for method in [
        "branch.review",
        "branch.review_counts",
        "branch.merge_prepare",
        "branch.merge_dangling",
    ] {
        let route = ROUTES
            .iter()
            .find(|route| route.wire == method)
            .ok_or_else(|| format!("no row for {method}"))?;
        assert_eq!(
            (route.action, route.class),
            ("read", Class::Hot),
            "{method}"
        );
        assert!(writer.contains(route.action), "a writer cannot {method}");
    }
    assert!(!writer.contains("merge"), "a writer merges");
    assert!(
        ROUTES
            .iter()
            .filter(|route| route.action == "merge")
            .all(|route| !writer.contains(route.action)),
        "a route a writer reaches carries merge"
    );
    Ok(())
}