shared-context-engineering 0.3.3

Shared Context Engineering CLI
//! Renderers for `sce trace status --all` (text and JSON).

use anyhow::{Context, Result};
use serde_json::json;

use crate::services::output_format::OutputFormat;
use crate::services::style;
use crate::services::trace::status_all::{DatabaseRow, DatabaseRowStatus, StatusAllReport};
use crate::services::trace::NAME;

const HEADING: &str = "SCE trace status (all)";
const TOTALS_HEADING: &str = "Totals";
const BY_DATABASE_HEADING: &str = "By database";

const COL_ALIAS: &str = "Alias";
const COL_SCOPE: &str = "Scope";
const COL_ID: &str = "ID";
const COL_STATUS: &str = "Status";
const COL_DIFFS: &str = "Diffs";
const COL_MESSAGES: &str = "Messages";
const COL_PARTS: &str = "Parts";
const COL_TRACES: &str = "Traces";
const COL_INTERSECTIONS: &str = "Intersections";
const SKIPPED_PLACEHOLDER: &str = "-";

pub fn render(report: &StatusAllReport, format: OutputFormat) -> Result<String> {
    match format {
        OutputFormat::Text => Ok(render_text(report)),
        OutputFormat::Json => render_json(report),
    }
}

fn render_text(report: &StatusAllReport) -> String {
    let mut lines = vec![style::heading(HEADING)];
    lines.push(format!(
        "Databases: {} discovered, {} ready, {} skipped",
        report.discovery.discovered, report.discovery.ready, report.discovery.skipped
    ));

    lines.push(String::new());
    lines.push(style::heading(TOTALS_HEADING));
    lines.push(format!("Diff traces: {}", report.totals.diff_traces));
    lines.push(format!("Messages: {}", report.totals.messages));
    lines.push(format!("Parts: {}", report.totals.parts));
    lines.push(format!("Agent traces: {}", report.totals.agent_traces));
    lines.push(format!(
        "Post-commit intersections: {}",
        report.totals.post_commit_patch_intersections
    ));
    lines.push(format!(
        "Last activity: {}",
        report
            .totals
            .last_activity
            .map_or_else(|| String::from("never"), |dt| dt.to_rfc3339())
    ));

    if !report.databases.is_empty() {
        lines.push(String::new());
        lines.push(style::heading(BY_DATABASE_HEADING));

        let headers = [
            COL_ALIAS,
            COL_SCOPE,
            COL_ID,
            COL_STATUS,
            COL_DIFFS,
            COL_MESSAGES,
            COL_PARTS,
            COL_TRACES,
            COL_INTERSECTIONS,
        ];
        let rows: Vec<[String; 9]> = report.databases.iter().map(format_row).collect();

        let widths: Vec<usize> = (0..headers.len())
            .map(|col| {
                rows.iter()
                    .map(|row| row[col].len())
                    .max()
                    .unwrap_or(0)
                    .max(headers[col].len())
            })
            .collect();

        lines.push(join_row(&headers.map(str::to_string), &widths));
        for row in &rows {
            lines.push(join_row(row, &widths));
        }
    }

    lines.join("\n")
}

fn join_row<const N: usize>(cells: &[String; N], widths: &[usize]) -> String {
    cells
        .iter()
        .enumerate()
        .map(|(i, cell)| format!("{cell:<width$}", width = widths[i]))
        .collect::<Vec<_>>()
        .join("  ")
        .trim_end()
        .to_string()
}

fn format_row(row: &DatabaseRow) -> [String; 9] {
    let scope = row.kind.label().to_string();
    let id = row.kind.identifier().to_string();

    match &row.status {
        DatabaseRowStatus::Ready { stats } => [
            row.alias.clone(),
            scope,
            id,
            "ready".to_string(),
            stats.diff_traces.to_string(),
            stats.messages.to_string(),
            stats.parts.to_string(),
            stats.agent_traces.to_string(),
            stats.post_commit_patch_intersections.to_string(),
        ],
        DatabaseRowStatus::Skipped { missing_table } => [
            row.alias.clone(),
            scope,
            id,
            format!("skipped: missing '{missing_table}'"),
            SKIPPED_PLACEHOLDER.to_string(),
            SKIPPED_PLACEHOLDER.to_string(),
            SKIPPED_PLACEHOLDER.to_string(),
            SKIPPED_PLACEHOLDER.to_string(),
            SKIPPED_PLACEHOLDER.to_string(),
        ],
    }
}

fn render_json(report: &StatusAllReport) -> Result<String> {
    let databases: Vec<serde_json::Value> = report
        .databases
        .iter()
        .map(|row| match &row.status {
            DatabaseRowStatus::Ready { stats } => json!({
                "alias": row.alias,
                "scope": row.kind.label(),
                "identifier": row.kind.identifier(),
                "path": row.path.display().to_string(),
                "status": "ready",
                "diff_traces": stats.diff_traces,
                "messages": stats.messages,
                "parts": stats.parts,
                "agent_traces": stats.agent_traces,
                "post_commit_patch_intersections": stats.post_commit_patch_intersections,
                "last_activity": stats
                    .last_activity
                    .map_or(serde_json::Value::Null, |dt| json!(dt.to_rfc3339())),
            }),
            DatabaseRowStatus::Skipped { missing_table } => json!({
                "alias": row.alias,
                "scope": row.kind.label(),
                "identifier": row.kind.identifier(),
                "path": row.path.display().to_string(),
                "status": "skipped",
                "skip_reason": format!("missing table: {missing_table}"),
            }),
        })
        .collect();

    let payload = json!({
        "status": "ok",
        "command": NAME,
        "subcommand": "status.all",
        "discovery": {
            "discovered": report.discovery.discovered,
            "ready": report.discovery.ready,
            "skipped": report.discovery.skipped,
        },
        "totals": {
            "diff_traces": report.totals.diff_traces,
            "messages": report.totals.messages,
            "parts": report.totals.parts,
            "agent_traces": report.totals.agent_traces,
            "post_commit_patch_intersections": report.totals.post_commit_patch_intersections,
            "last_activity": report
                .totals
                .last_activity
                .map_or(serde_json::Value::Null, |dt| json!(dt.to_rfc3339())),
        },
        "databases": databases,
    });

    serde_json::to_string_pretty(&payload)
        .context("failed to serialize trace status.all report to JSON")
}

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

    use std::path::PathBuf;
    use std::time::{SystemTime, UNIX_EPOCH};

    use crate::services::trace::status_all::aggregate_status_all_in;

    fn unique_temp_dir(label: &str) -> PathBuf {
        let nonce = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("system time should be after Unix epoch")
            .as_nanos();
        let dir = std::env::temp_dir().join(format!(
            "sce-trace-render-status-all-{label}-{}-{nonce}",
            std::process::id()
        ));
        std::fs::create_dir_all(&dir).expect("create temp dir");
        dir
    }

    #[test]
    fn empty_renders_text_with_zeroed_summary_and_totals() {
        let dir = unique_temp_dir("empty-text");
        let report = aggregate_status_all_in(&dir).expect("aggregate");
        let rendered = render_text(&report);
        assert!(rendered.contains("Databases: 0 discovered, 0 ready, 0 skipped"));
        assert!(rendered.contains("Diff traces: 0"));
        assert!(rendered.contains("Last activity: never"));
        assert!(!rendered.contains(BY_DATABASE_HEADING));
    }

    #[test]
    fn empty_renders_json_with_zeroed_shape() {
        let dir = unique_temp_dir("empty-json");
        let report = aggregate_status_all_in(&dir).expect("aggregate");
        let payload = render_json(&report).expect("json render");
        let value: serde_json::Value = serde_json::from_str(&payload).expect("valid json");
        assert_eq!(value["status"], "ok");
        assert_eq!(value["command"], "trace");
        assert_eq!(value["subcommand"], "status.all");
        assert_eq!(value["discovery"]["discovered"], 0);
        assert_eq!(value["discovery"]["ready"], 0);
        assert_eq!(value["discovery"]["skipped"], 0);
        assert_eq!(value["totals"]["diff_traces"], 0);
        assert!(value["totals"]["last_activity"].is_null());
        assert_eq!(value["databases"].as_array().unwrap().len(), 0);
    }
}