assay-cli 3.19.0

CLI for Assay
use serde_json::Value;

fn as_string_array(value: Option<&Value>) -> Vec<String> {
    match value {
        Some(Value::Array(items)) => items
            .iter()
            .filter_map(Value::as_str)
            .map(ToOwned::to_owned)
            .collect(),
        _ => Vec::new(),
    }
}

pub(crate) fn render_coverage_markdown(
    report: &Value,
    routes_top: usize,
) -> Result<String, String> {
    let schema_version = report
        .get("schema_version")
        .and_then(Value::as_str)
        .ok_or_else(|| "missing schema_version".to_string())?;
    let report_version = report
        .get("report_version")
        .and_then(Value::as_str)
        .unwrap_or("1");
    let source = report
        .pointer("/run/source")
        .and_then(Value::as_str)
        .unwrap_or("unknown");

    let tools_seen = as_string_array(report.pointer("/tools/tools_seen"));
    let tools_declared = as_string_array(report.pointer("/tools/tools_declared"));
    let tools_unknown = as_string_array(report.pointer("/tools/tools_unknown"));
    let taxonomy_missing = as_string_array(report.pointer("/taxonomy/tool_classes_missing"));

    let mut routes: Vec<(String, String, u64)> = match report.pointer("/routes/routes_seen") {
        Some(Value::Array(items)) => items
            .iter()
            .filter_map(|item| {
                let from = item.get("from")?.as_str()?.to_string();
                let to = item.get("to")?.as_str()?.to_string();
                let count = item.get("count")?.as_u64()?;
                Some((from, to, count))
            })
            .collect(),
        _ => Vec::new(),
    };

    routes.sort_by(|a, b| {
        b.2.cmp(&a.2)
            .then_with(|| a.0.cmp(&b.0))
            .then_with(|| a.1.cmp(&b.1))
    });

    let mut out = String::new();
    out.push_str("# Coverage Report\n\n");
    out.push_str(&format!("- Schema: `{}`\n", schema_version));
    out.push_str(&format!("- Report version: `{}`\n", report_version));
    out.push_str(&format!("- Source: `{}`\n\n", source));

    out.push_str("## Tools\n\n");
    out.push_str("| Metric | Value |\n");
    out.push_str("| --- | ---: |\n");
    out.push_str(&format!("| tools_seen | {} |\n", tools_seen.len()));
    out.push_str(&format!("| tools_declared | {} |\n", tools_declared.len()));
    out.push_str(&format!("| tools_unknown | {} |\n\n", tools_unknown.len()));

    out.push_str("## Unknown Tools\n\n");
    if tools_unknown.is_empty() {
        out.push_str("- _none_\n\n");
    } else {
        for tool in &tools_unknown {
            out.push_str(&format!("- `{}`\n", tool));
        }
        out.push('\n');
    }

    out.push_str("## Taxonomy Missing\n\n");
    if taxonomy_missing.is_empty() {
        out.push_str("- _none_\n\n");
    } else {
        for tool in &taxonomy_missing {
            out.push_str(&format!("- `{}`\n", tool));
        }
        out.push('\n');
    }

    out.push_str("## Top Routes\n\n");
    if routes_top == 0 {
        out.push_str("(routes hidden; --routes-top=0)\n\n");
    } else {
        out.push_str("| From | To | Count |\n");
        out.push_str("| --- | --- | ---: |\n");
        for (from, to, count) in routes.into_iter().take(routes_top) {
            out.push_str(&format!("| `{}` | `{}` | {} |\n", from, to, count));
        }
        out.push('\n');
    }

    out.push_str("## Notes\n\n");
    out.push_str("- Routes are adjacent tool-call edges in observed order (v1).\n");
    out.push_str("- This markdown is a presentation of `coverage_report_v1`; enforcement behavior is unchanged.\n");

    Ok(out)
}