haematite-cli 0.21.1

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
Documentation
//! Every served route, classified once (ACCESS-001 R1, PERMISSIONS-ACROSS-THE-STACK D2).
//!
//! A row names the Lys resource kind a route acts on, the action it needs and
//! the class that decides it: a hot route is decided from the caller's pass
//! alone and never asks Lys; a deliberate route asks Lys live and is refused
//! "Lys could not be asked" when Lys cannot answer, never falling back to the
//! pass. The class is fixed per action, never chosen per request.
//!
//! `hello` negotiates the protocol and `session.pass` presents the
//! connection's pass; neither touches a resource. `query.check` and
//! `query.words` describe the language without touching a resource. `mutation.apply` is an envelope decided by the change it carries, so
//! neither has a row of its own. docs/design/access/MATRIX.md is the rendering
//! of these rows and is checked against them.

/// How a route's right is decided.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Class {
    /// From the caller's pass alone, offline.
    Hot,
    /// By asking Lys live, with the pass as the caller.
    Deliberate,
}

impl Class {
    /// The word the matrix and the Lys schema use.
    #[must_use]
    pub const fn word(self) -> &'static str {
        match self {
            Self::Hot => "hot",
            Self::Deliberate => "deliberate",
        }
    }
}

/// One served route's classification.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Route {
    /// The method or change kind as it appears on the wire.
    pub wire: &'static str,
    /// The Lys resource kind under `haematite.`, as docs/design/access/schema.json declares it.
    pub kind: &'static str,
    /// The action the caller needs on that resource.
    pub action: &'static str,
    /// How the right is decided.
    pub class: Class,
}

/// Served methods that carry no resource of their own.
pub const ENVELOPES: &[&str] = &["hello", "session.pass", "query.check", "query.words"];

const fn hot(wire: &'static str, kind: &'static str, action: &'static str) -> Route {
    Route {
        wire,
        kind,
        action,
        class: Class::Hot,
    }
}

const fn deliberate(wire: &'static str, kind: &'static str, action: &'static str) -> Route {
    Route {
        wire,
        kind,
        action,
        class: Class::Deliberate,
    }
}

/// Every served route, reads first in protocol order, then change kinds.
pub const ROUTES: &[Route] = &[
    hot("branches.list", "database", "read"),
    hot("collections.list", "database", "read"),
    hot("documents.get", "collection", "read"),
    hot("documents.list", "collection", "read"),
    hot("history.list", "branch", "read"),
    hot("head.follow", "branch", "read"),
    hot("branch.review", "branch", "read"),
    hot("branch.review_counts", "branch", "read"),
    hot("branch.merge_prepare", "branch", "read"),
    hot("branch.merge_dangling", "branch", "read"),
    hot("mutation.receipt", "database", "read"),
    deliberate("drafts.run", "database", "serve"),
    hot("counts.rebuild", "branch", "write"),
    hot("counts.audit", "branch", "read"),
    hot("edge.out", "collection", "read"),
    hot("edge.in", "collection", "read"),
    hot("graph.export", "database", "read"),
    hot("vectors.get", "collection", "read"),
    hot("vectors.list", "collection", "read"),
    hot("vectors.search", "collection", "query"),
    hot("vectors.search_text", "collection", "query"),
    hot("store.stats", "database", "read"),
    hot("vectors.fields", "database", "read"),
    hot("embed.rewalk_longer", "collection", "write"),
    hot("relations.list", "database", "read"),
    hot("query.run", "database", "query"),
    hot("changes.list", "database", "read"),
    hot("collection.put", "database", "write"),
    hot("document.put", "collection", "write"),
    hot("document.append_prune", "collection", "write"),
    hot("document.delete", "collection", "write"),
    deliberate("branch.fork", "database", "branch"),
    deliberate("branch.discard", "database", "branch"),
    deliberate("branch.merge", "branch", "merge"),
    // A change also writes its branch, a resource placed under the database
    // that inherits from it unless placed restricted (Waffles, 12 Oct 2026).
    hot("mutation.apply", "branch", "write"),
    hot("edge.put", "collection", "write"),
    hot("edge.delete", "collection", "write"),
    hot("index.declare", "collection", "write"),
    hot("vector.declare", "collection", "write"),
    hot("vector.put", "collection", "write"),
    hot("vector.delete", "collection", "write"),
    hot("documents.put_many", "collection", "write"),
    hot("documents.delete_many", "collection", "write"),
    hot("vectors.put_many", "collection", "write"),
    hot("edges.put_many", "collection", "write"),
    hot("relation.put", "database", "write"),
    hot("relation.delete", "database", "write"),
];

/// The row for a served route, or `None` for an envelope or an unknown name.
#[must_use]
pub fn route(wire: &str) -> Option<&'static Route> {
    ROUTES.iter().find(|route| route.wire == wire)
}

/// docs/design/access/MATRIX.md, rendered from [`ROUTES`].
#[must_use]
pub fn render() -> String {
    let mut out = String::from(
        "# Haematite served route matrix\n\n\
         Rendered from `crates/haematite-cli/src/serve/route_matrix.rs` and checked against it by\n\
         `route_matrix_tests.rs`; edit the rows, never this file. A hot route is decided from\n\
         the caller's Lys pass alone; a deliberate route asks Lys live and is refused \"Lys\n\
         could not be asked\" when it cannot (PERMISSIONS-ACROSS-THE-STACK D2). `hello` and\n\
         `session.pass` carry no resource. A mutation is decided by its change kind and, for\n\
         a change to the branch's data, by `mutation.apply` on the branch it writes.\n\n\
         | Route | Resource | Action | Class |\n\
         | --- | --- | --- | --- |\n",
    );
    for route in ROUTES {
        for cell in [
            "| `",
            route.wire,
            "` | haematite.",
            route.kind,
            " | ",
            route.action,
            " | ",
            route.class.word(),
            " |\n",
        ] {
            out.push_str(cell);
        }
    }
    out
}

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