kiromi-ai-cli 0.2.2

Operator and developer CLI for the kiromi-ai-memory store: append, search, snapshot, regenerate, migrate-scheme, gc, audit-tail.
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Output formatting — table (default) + JSON (`--json`).

use comfy_table::{ContentArrangement, Table};
use kiromi_ai_memory::{MemoryRecord, MemoryRef, SearchHit};
use serde::Serialize;

/// Render a list of memory refs as a table.
#[must_use]
pub(crate) fn refs_table(refs: &[MemoryRef]) -> String {
    let mut t = Table::new();
    t.set_content_arrangement(ContentArrangement::Dynamic);
    t.set_header(vec!["id", "partition"]);
    for r in refs {
        t.add_row(vec![r.id.to_string(), r.partition.as_str().to_string()]);
    }
    t.to_string()
}

/// Render search hits.
#[must_use]
pub(crate) fn hits_table(hits: &[SearchHit]) -> String {
    let mut t = Table::new();
    t.set_content_arrangement(ContentArrangement::Dynamic);
    t.set_header(vec!["score", "id", "partition"]);
    for h in hits {
        t.add_row(vec![
            format!("{:.4}", h.score),
            h.r#ref.id.to_string(),
            h.r#ref.partition.as_str().to_string(),
        ]);
    }
    t.to_string()
}

/// Render one memory record as a key/value table.
#[must_use]
pub(crate) fn record_kv(r: &MemoryRecord) -> String {
    let mut t = Table::new();
    t.set_content_arrangement(ContentArrangement::Dynamic);
    t.add_row(vec!["id".to_string(), r.r#ref.id.to_string()]);
    t.add_row(vec!["partition".to_string(), r.r#ref.partition.to_string()]);
    t.add_row(vec![
        "created_at_ms".to_string(),
        r.created_at_ms.to_string(),
    ]);
    t.add_row(vec![
        "updated_at_ms".to_string(),
        r.updated_at_ms.to_string(),
    ]);
    t.add_row(vec!["tombstoned".to_string(), r.tombstoned.to_string()]);
    t.to_string()
}

/// JSON-pretty-serialise any `Serialize`. Falls back to `"null"` if encoding fails
/// (in practice, only on cyclic graphs — none of our wire types contain cycles).
#[must_use]
pub(crate) fn to_json<T: Serialize>(v: &T) -> String {
    match serde_json::to_string_pretty(v) {
        Ok(s) => s,
        Err(_) => "null".into(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn refs_table_handles_empty() {
        let s = refs_table(&[]);
        assert!(s.contains("id"));
        assert!(s.contains("partition"));
    }

    #[test]
    fn hits_table_handles_empty() {
        let s = hits_table(&[]);
        assert!(s.contains("score"));
    }

    #[test]
    fn to_json_pretty_prints() {
        let v = serde_json::json!({"a": 1});
        let s = to_json(&v);
        assert!(s.contains("\"a\""));
    }
}