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::num::NonZeroU32;

use serde::Deserialize;

use super::read_support::{Snapshot, bounded, call, failure};
use super::review_lines;
use super::{EncodeError, Sender, ServiceRefusal, ToolResult, refused};
use crate::mcp::{rpc, types};
use crate::serve::protocol;

#[derive(Deserialize)]
struct Difference {
    selection: Snapshot,
    from: Snapshot,
    diff: Vec<review_lines::Difference>,
    next_after: Option<String>,
    base_moved: Option<u64>,
}

#[derive(Deserialize)]
struct Partial {
    changes: u64,
    conflicts: u64,
}

#[derive(Deserialize)]
struct Counted {
    selection: Snapshot,
    from: Snapshot,
    parent: Snapshot,
    partial: Partial,
    base_moved: u64,
    done: bool,
    next_after: Option<String>,
}

#[derive(Deserialize)]
struct Prepared<T> {
    selection: Snapshot,
    from: Snapshot,
    parent: Snapshot,
    lines: Vec<T>,
    done: bool,
    next_after: Option<String>,
}

impl<T> Prepared<T> {
    fn require(
        &self,
        selected: &Snapshot,
        from: &Snapshot,
        target: &Snapshot,
    ) -> Result<(), ServiceRefusal> {
        self.selection.require(selected)?;
        self.from.require(from)?;
        self.parent.require(target)?;
        complete(self.done, self.next_after.as_deref())
    }
}

struct Review {
    difference: Difference,
    counted: Counted,
    prepared: Prepared<review_lines::Conflict>,
    dangling: Prepared<types::DanglingLine>,
}

fn complete(done: bool, next: Option<&str>) -> Result<(), ServiceRefusal> {
    if done == next.is_none() {
        Ok(())
    } else {
        Err(failure(
            "mcp_service_answer_invalid",
            "The served completion flag contradicts its continuation",
        ))
    }
}

fn request(
    input: &types::ReviewBranchInput,
    after: Option<String>,
    limit: NonZeroU32,
) -> protocol::ReviewPage {
    protocol::ReviewPage {
        branch: input.selection.branch.clone(),
        after,
        limit,
    }
}

fn read(
    sender: &mut impl Sender,
    input: &types::ReviewBranchInput,
    limit: NonZeroU32,
) -> Result<Review, ServiceRefusal> {
    let cursors = &input.after;
    let difference: Difference = call(
        sender,
        protocol::Operation::Review(request(input, cursors.diff.clone(), limit)),
    )?;
    difference.selection.requested(&input.selection)?;
    let counted: Counted = call(
        sender,
        protocol::Operation::ReviewCounts(request(input, cursors.counts.clone(), limit)),
    )?;
    counted.selection.require(&difference.selection)?;
    counted.from.require(&difference.from)?;
    complete(counted.done, counted.next_after.as_deref())?;
    if difference
        .base_moved
        .is_some_and(|moved| moved != counted.base_moved)
    {
        return Err(failure(
            "mcp_service_snapshot_mismatch",
            "The target moved between served review pages",
        ));
    }
    let prepared: Prepared<review_lines::Conflict> = call(
        sender,
        protocol::Operation::MergePrepare(request(input, cursors.conflicts.clone(), limit)),
    )?;
    prepared.require(&difference.selection, &difference.from, &counted.parent)?;
    let dangling: Prepared<types::DanglingLine> = call(
        sender,
        protocol::Operation::MergeDangling(request(input, cursors.dangling.clone(), limit)),
    )?;
    dangling.require(&difference.selection, &difference.from, &counted.parent)?;
    Ok(Review {
        difference,
        counted,
        prepared,
        dangling,
    })
}

fn project(review: Review) -> Result<types::ReviewBranchOutput, ServiceRefusal> {
    Ok(types::ReviewBranchOutput {
        selection: review.difference.selection.output(),
        from: review.difference.from.output(),
        target: review.counted.parent.output(),
        diff: types::Page {
            entries: review
                .difference
                .diff
                .into_iter()
                .map(review_lines::difference)
                .collect::<Result<Vec<_>, _>>()?,
            next_after: review.difference.next_after,
        },
        counts: types::ReviewCountPage {
            partial: types::ReviewCounts {
                changes: review.counted.partial.changes,
                conflicts: review.counted.partial.conflicts,
            },
            next_after: review.counted.next_after,
        },
        conflicts: types::Page {
            entries: review
                .prepared
                .lines
                .into_iter()
                .map(review_lines::conflict)
                .collect::<Result<Vec<_>, _>>()?,
            next_after: review.prepared.next_after,
        },
        dangling: types::Page {
            entries: review.dangling.lines,
            next_after: review.dangling.next_after,
        },
        base_moved: review.difference.base_moved,
    })
}

fn review(
    sender: &mut impl Sender,
    input: &types::ReviewBranchInput,
) -> Result<ToolResult, ServiceRefusal> {
    let limit = input.page.rows;
    let cursors = &input.after;
    if input.selection.version.is_none()
        && (cursors.diff.is_some()
            || cursors.counts.is_some()
            || cursors.conflicts.is_some()
            || cursors.dangling.is_some())
    {
        return Err(failure(
            "mcp_version_required",
            "Review continuation requires the captured selection version",
        ));
    }
    let output = project(read(sender, input, limit)?)?;
    bounded(&output, input.page.bytes.get())
}

pub(super) fn dispatch(
    sender: &mut impl Sender,
    request: &rpc::ToolCall,
) -> Result<ToolResult, EncodeError> {
    let input = match serde_json::from_str(request.arguments.get()) {
        Ok(input) => input,
        Err(error) => return refused(&failure("mcp_arguments_invalid", error.to_string())),
    };
    match review(sender, &input) {
        Ok(answer) => Ok(answer),
        Err(error) => refused(&error),
    }
}