rdar 0.6.9

radar - the repository cartographer for AI agents: compiles a repo into tiny committed MAP.md routers, with measured token benchmarks
Documentation
//! `.radar/SYMBOLS.md` - a committed-format, ctags-style exact-symbol index
//! (research pass: LocAgent's ablation found keyword→entity lookup the
//! single most impactful navigation tool; sorted text beats bloom filters -
//! zero false positives, offset-readable, greppable under the map-directed
//! contract).
//!
//! One line per public symbol name: `name<TAB>file#line ...`, sorted by name.
//! Regenerated on every map/refresh; capped by rank when enormous.

use std::collections::BTreeMap;
use std::io;
use std::path::Path;

use crate::cache::ScanCache;
use crate::extract::Vis;
use crate::graph::Ranking;
use crate::place::Placement;

/// Beyond this many lines, keep the top-ranked half and say so (steering
/// line instead of silent truncation).
const MAX_LINES: usize = 5000;

pub fn write(
    root: &Path,
    cache: &ScanCache,
    _placement: &Placement,
    ranking: &Ranking,
) -> io::Result<usize> {
    let mut lines: Vec<(String, u64)> = Vec::new();
    for (rel, entry) in &cache.files {
        let Some(lang) = entry.lang else { continue };
        let Some(x) = cache.parses.get(&(lang, entry.hash)) else {
            continue;
        };
        for d in &x.defs {
            if d.vis != Vis::Pub {
                continue;
            }
            let rank = ranking.name_refs.get(&d.name).copied().unwrap_or(0);
            lines.push((format!("{}\t{rel}#{}", d.name, d.line), rank));
        }
    }
    lines.sort();
    lines.dedup();
    let total = lines.len();
    let mut kept: Vec<String> = if total > MAX_LINES {
        // Keep the highest-ranked; preserve alphabetical order for lookup.
        let mut by_rank = lines.clone();
        by_rank.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
        let cutoff: std::collections::BTreeSet<&String> =
            by_rank.iter().take(MAX_LINES).map(|(l, _)| l).collect();
        lines
            .iter()
            .filter(|(l, _)| cutoff.contains(l))
            .map(|(l, _)| l.clone())
            .collect()
    } else {
        lines.into_iter().map(|(l, _)| l).collect()
    };

    let mut doc = String::from(
        "# SYMBOLS - exact public-symbol index\n\
         # symbol\tanchors\n",
    );
    if total > kept.len() {
        doc.push_str(&format!(
            "# note: {} lower-ranked symbols omitted - search source within the routed scope\n",
            total - kept.len()
        ));
    }
    let mut grouped: BTreeMap<String, Vec<String>> = BTreeMap::new();
    for line in kept.drain(..) {
        if let Some((symbol, anchor)) = line.split_once('\t') {
            grouped
                .entry(symbol.to_string())
                .or_default()
                .push(anchor.to_string());
        }
    }
    for (symbol, anchors) in grouped {
        doc.push_str(&symbol);
        doc.push('\t');
        doc.push_str(&anchors.join(" "));
        doc.push('\n');
    }
    let dir = crate::cache::radar_dir(root);
    std::fs::create_dir_all(&dir)?;
    std::fs::write(dir.join("SYMBOLS.md"), doc)?;
    Ok(total)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cache::FileEntry;
    use crate::extract::{Extraction, SymKind, Symbol};
    use crate::lang::Lang;

    #[test]
    fn writes_sorted_exact_index() {
        let dir = std::env::temp_dir().join(format!("radar-symbols-{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&dir);
        std::fs::create_dir_all(&dir).expect("mkdir");
        let mut cache = ScanCache::default();
        let hash = [1u8; 32];
        cache.files.insert(
            "pkg/a.py".into(),
            FileEntry {
                mtime: (1, 0),
                size: 1,
                ino: 0,
                hash,
                lang: Some(Lang::Python),
            },
        );
        cache.files.insert(
            "pkg/b.py".into(),
            FileEntry {
                mtime: (1, 0),
                size: 1,
                ino: 0,
                hash: [2u8; 32],
                lang: Some(Lang::Python),
            },
        );
        cache.parses.insert(
            (Lang::Python, hash),
            Extraction {
                defs: vec![
                    Symbol {
                        line: 3,
                        end_line: 4,
                        name: "zeta".into(),
                        kind: SymKind::Fn,
                        vis: Vis::Pub,
                        sig: "def zeta()".into(),
                        terms: Vec::new(),
                    },
                    Symbol {
                        line: 9,
                        end_line: 10,
                        name: "alpha".into(),
                        kind: SymKind::Fn,
                        vis: Vis::Pub,
                        sig: "def alpha()".into(),
                        terms: Vec::new(),
                    },
                    Symbol {
                        line: 12,
                        end_line: 13,
                        name: "_hidden".into(),
                        kind: SymKind::Fn,
                        vis: Vis::Priv,
                        sig: "def _hidden()".into(),
                        terms: Vec::new(),
                    },
                ],
                refs: vec![],
            },
        );
        cache.parses.insert(
            (Lang::Python, [2u8; 32]),
            Extraction {
                defs: vec![Symbol {
                    line: 2,
                    end_line: 3,
                    name: "alpha".into(),
                    kind: SymKind::Fn,
                    vis: Vis::Pub,
                    sig: "def alpha()".into(),
                    terms: Vec::new(),
                }],
                refs: vec![],
            },
        );
        let placement = crate::place::place(&cache);
        let ranking = crate::graph::rank(&cache);
        let total = write(&dir, &cache, &placement, &ranking).expect("write");
        assert_eq!(total, 3, "private symbols excluded");
        let doc = std::fs::read_to_string(dir.join(".radar/SYMBOLS.md")).expect("read");
        let alpha = doc.find("alpha\t").expect("alpha indexed");
        let zeta = doc.find("zeta\t").expect("zeta indexed");
        assert!(alpha < zeta, "sorted");
        assert!(doc.contains("# symbol\tanchors"), "compact header: {doc}");
        assert!(!doc.contains("\tMAP.md\t"), "map column omitted: {doc}");
        assert!(
            doc.contains("alpha\tpkg/a.py#9 pkg/b.py#2"),
            "duplicate symbol anchors grouped: {doc}"
        );
        let _ = std::fs::remove_dir_all(&dir);
    }
}