rdar 0.6.11

radar - the repository cartographer for AI agents: compiles a repo into tiny committed MAP.md routers, with measured token benchmarks
Documentation
//! `radar tree` - bash-`tree`-style topology view of the maps:
//! bare minimum per node - scope, fidelity, ~tokens, staleness marker.

use std::collections::BTreeMap;

use crate::check::{CheckReport, DiskMap};

pub struct TreeOpts {
    pub ascii: bool,
    pub depth: Option<usize>,
}

/// Render the topology. Staleness markers come from the check report.
pub fn render(report: &CheckReport, opts: &TreeOpts) -> String {
    let maps = &report.maps;
    if maps.is_empty() {
        return "no maps - run `radar map` first\n".to_string();
    }
    // scope → direct child scopes (by nearest-ancestor relation).
    let mut kids: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
    for scope in maps.keys() {
        if scope.is_empty() {
            continue;
        }
        let parent = nearest_ancestor(maps, scope).unwrap_or("");
        kids.entry(parent).or_default().push(scope);
    }

    let mut out = String::new();
    let shared_fidelity = maps
        .values()
        .next()
        .filter(|first| {
            (opts.depth.is_none()
                || opts.depth.is_some_and(|depth| {
                    depth > 0 && kids.get("").is_some_and(|children| children.len() >= 2)
                }))
                && maps.len() >= 3
                && maps.contains_key("")
                && maps.values().all(|map| map.fidelity == first.fidelity)
        })
        .map(|map| map.fidelity.as_str());
    if let Some(fidelity) = shared_fidelity {
        out.push_str(&format!("fidelity: {fidelity}\n"));
    }
    let name_width = maps
        .keys()
        .map(|s| display_name(s, maps).len() + s.matches('/').count() * 4)
        .max()
        .unwrap_or(8)
        .max(8);
    render_node(
        report,
        &kids,
        "",
        "",
        true,
        true,
        opts,
        shared_fidelity,
        name_width,
        0,
        &mut out,
    );
    out
}

fn display_name<'a>(scope: &'a str, maps: &BTreeMap<String, DiskMap>) -> &'a str {
    if scope.is_empty() {
        return ".";
    }
    // Show the path segment relative to the parent map's scope.
    match nearest_ancestor(maps, scope) {
        Some(p) if !p.is_empty() => scope
            .strip_prefix(p)
            .map_or(scope, |s| s.trim_start_matches('/')),
        _ => scope,
    }
}

/// Find the nearest mapped ancestor used as a parent during tree rendering.
fn nearest_ancestor<'m>(maps: &'m BTreeMap<String, DiskMap>, scope: &str) -> Option<&'m str> {
    let mut cur = scope;
    loop {
        let parent = match cur.rfind('/') {
            Some(i) => &cur[..i],
            None if !cur.is_empty() => "",
            None => return None,
        };
        if let Some((k, _)) = maps.get_key_value(parent) {
            return Some(k.as_str());
        }
        if parent.is_empty() {
            return None;
        }
        cur = parent;
    }
}

#[allow(clippy::too_many_arguments)]
fn render_node(
    report: &CheckReport,
    kids: &BTreeMap<&str, Vec<&str>>,
    scope: &str,
    prefix: &str,
    is_last: bool,
    is_root: bool,
    opts: &TreeOpts,
    shared_fidelity: Option<&str>,
    name_width: usize,
    depth: usize,
    out: &mut String,
) {
    let Some(map) = report.maps.get(scope) else {
        return;
    };
    let (tee, ell, pipe) = if opts.ascii {
        ("|-- ", "`-- ", "|   ")
    } else {
        ("├── ", "└── ", "")
    };
    let connector = if is_root {
        ""
    } else if is_last {
        ell
    } else {
        tee
    };
    let name = display_name(scope, &report.maps);
    let stale = report.stale.get(scope).copied().unwrap_or(false);
    let marker = match (stale, opts.ascii) {
        (true, false) => "✗ stale",
        (true, true) => "x stale",
        (false, false) => "",
        (false, true) => "ok",
    };
    let tokens = map.tokens.as_deref().unwrap_or("?");
    let label = format!("{prefix}{connector}{name}");
    let pad = name_width.saturating_sub(label.chars().count()) + 2;
    if shared_fidelity.is_some() {
        out.push_str(&format!("{label}{:pad$}{:>7}  {marker}\n", "", tokens));
    } else {
        out.push_str(&format!(
            "{label}{:pad$}{:<8} {:>7}  {marker}\n",
            "", map.fidelity, tokens,
        ));
    }

    if opts.depth.is_some_and(|d| depth + 1 > d) {
        return;
    }
    let children = kids.get(scope).cloned().unwrap_or_default();
    let child_prefix = if is_root {
        String::new()
    } else if is_last {
        format!("{prefix}    ")
    } else {
        format!("{prefix}{pipe}")
    };
    for (i, child) in children.iter().enumerate() {
        let last = i + 1 == children.len();
        render_node(
            report,
            kids,
            child,
            &child_prefix,
            last,
            false,
            opts,
            shared_fidelity,
            name_width,
            depth + 1,
            out,
        );
    }
}

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

    fn map(fidelity: &str) -> DiskMap {
        DiskMap {
            parent_link: None,
            children_links: Vec::new(),
            api_hash: None,
            bytes_actual: 0,
            tokens: Some("~10".to_string()),
            fidelity: fidelity.to_string(),
            slot_filled: true,
        }
    }

    #[test]
    fn shared_fidelity_requires_three_visible_uniform_rows() {
        let mut report = CheckReport::default();
        report.maps.insert(String::new(), map("syntax"));
        report.maps.insert("a".to_string(), map("syntax"));
        report.maps.insert("b".to_string(), map("syntax"));
        report.stale.insert("a".to_string(), true);

        let mut opts = TreeOpts {
            ascii: true,
            depth: None,
        };
        let compact = render(&report, &opts);
        assert!(compact.starts_with("fidelity: syntax\n"));
        assert_eq!(compact.matches("syntax").count(), 1);
        assert!(compact.contains("|-- a"));
        assert!(compact.contains("`-- b"));
        assert!(compact.contains("x stale"));

        opts.depth = Some(1);
        let bounded = render(&report, &opts);
        assert!(bounded.starts_with("fidelity: syntax\n"));
        assert_eq!(bounded.matches("syntax").count(), 1);

        opts.depth = Some(0);
        assert!(!render(&report, &opts).starts_with("fidelity:"));

        report.maps.remove("b");
        opts.depth = Some(1);
        assert!(!render(&report, &opts).starts_with("fidelity:"));

        report.maps.insert("b".to_string(), map("lsp"));
        opts.depth = None;
        let mixed = render(&report, &opts);
        assert!(!mixed.starts_with("fidelity:"));
        assert!(mixed.contains("syntax"));
        assert!(mixed.contains("lsp"));

        report.maps.remove("");
        assert_eq!(render(&report, &opts), "");
    }
}