haematite-cli 0.21.0

The haematite command: serve a store, its door and Studio, answer MCP, query with jq, and host the store's assistant seat
//! Human-readable rendering of a [`MigrationReport`] (CHUNKING-POLICY.md §4.2).
//!
//! Split from [`crate::render`] to keep that file under the line cap.

use std::fmt::{self, Write as _};

use haematite::db::migrate::{MigrationDirection, MigrationOutcome, MigrationReport};

pub fn render(report: &MigrationReport) -> Result<String, fmt::Error> {
    let mut out = String::new();
    let banner = match report.outcome {
        MigrationOutcome::AlreadyInTargetFormat => {
            "haematite migrate-chunking — already in target format, nothing to do"
        }
        MigrationOutcome::MigratedToV2 => {
            "haematite migrate-chunking — migrated to v2 (byte-aware)"
        }
        MigrationOutcome::AbortedToV1 => {
            "haematite migrate-chunking — reverted to v1 (count-target)"
        }
    };
    let direction = match report.direction {
        MigrationDirection::Forward => "forward (v1 → v2)",
        MigrationDirection::Abort => "abort/reverse (v2 → v1)",
    };
    writeln!(out, "{banner}")?;
    writeln!(out, "data dir:    {}", report.data_dir.display())?;
    writeln!(
        out,
        "direction:   {direction}  ({} → {})",
        report.source_policy, report.target_policy
    )?;
    writeln!(
        out,
        "shard count: {} · duration: {} ms",
        report.shard_count, report.duration_ms
    )?;
    out.push('\n');
    writeln!(
        out,
        "shards:  rebuilt {} · unchanged {} · empty {}",
        group(report.shards_rebuilt),
        group(report.shards_unchanged),
        group(report.shards_empty)
    )?;
    writeln!(
        out,
        "branch heads:  rebuilt {} · unchanged {}",
        group(report.branch_heads_rebuilt),
        group(report.branch_heads_unchanged)
    )?;
    out.push('\n');
    writeln!(
        out,
        "vacuum-before-migrate:\n  {}",
        report.vacuum_before_migrate
    )?;
    writeln!(out, "manifest bracket:\n  {}", report.manifest_bracket)?;
    Ok(out)
}

/// Thousands-grouped integer (a private copy of `render`'s helper — the two
/// renderers live in separate files to stay under the line cap).
fn group(value: usize) -> String {
    let digits = value.to_string();
    let mut grouped = String::with_capacity(digits.len() + digits.len() / 3);
    for (index, digit) in digits.chars().enumerate() {
        if index > 0 && (digits.len() - index).is_multiple_of(3) {
            grouped.push(',');
        }
        grouped.push(digit);
    }
    grouped
}