rdar 0.6.12

radar - the repository cartographer for AI agents: compiles a repo into tiny committed MAP.md routers, with measured token benchmarks
Documentation
//! `radar.toml` - a flat, honest TOML subset: `[section]`
//! headers, `key = "string"`, `key = ["a", "b"]`, `key = 123`, `key = true`,
//! `#` comments. Every key radar writes is a key radar honors; nesting and
//! exotic TOML are rejected by omission (unknown lines are ignored with the
//! degrade-don't-block stance - a hand-written full-TOML file still parses
//! for the flat keys we care about).

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

#[derive(Debug, Clone)]
pub struct Config {
    /// `[agent] cmd = "claude"` - the launcher command (§6.2).
    pub agent_cmd: Option<String>,
    /// `[routes] enabled` - force-disable ALL automatic route-cache action.
    pub routes_enabled: bool,
    /// `[routes] auto` - radar's own deterministic route seeding.
    pub routes_auto: bool,
    /// All parsed `section.key` → raw value pairs (unquoted).
    pub raw: BTreeMap<String, String>,
}

impl Default for Config {
    fn default() -> Config {
        Config {
            agent_cmd: None,
            routes_enabled: true,
            routes_auto: true,
            raw: BTreeMap::new(),
        }
    }
}

impl Config {
    pub fn load(root: &Path) -> Config {
        let Ok(text) = std::fs::read_to_string(root.join("radar.toml")) else {
            return Config::default();
        };
        parse(&text)
    }
}

fn unquote(v: &str) -> String {
    let v = v.trim();
    v.strip_prefix('"')
        .and_then(|s| s.strip_suffix('"'))
        .unwrap_or(v)
        .to_string()
}

fn strip_comment(value: &str) -> &str {
    let mut quoted = false;
    let mut escaped = false;
    for (index, character) in value.char_indices() {
        if escaped {
            escaped = false;
            continue;
        }
        match character {
            '\\' if quoted => escaped = true,
            '"' => quoted = !quoted,
            '#' if !quoted => return value[..index].trim_end(),
            _ => {}
        }
    }
    value.trim()
}

pub fn parse(text: &str) -> Config {
    let mut raw = BTreeMap::new();
    let mut section = String::new();
    for line in text.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }
        if let Some(name) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
            section = name.trim().to_string();
            continue;
        }
        let Some((key, value)) = line.split_once('=') else {
            continue;
        };
        let full = if section.is_empty() {
            key.trim().to_string()
        } else {
            format!("{section}.{}", key.trim())
        };
        let value = strip_comment(value);
        raw.insert(full, unquote(value));
    }
    let flag = |key: &str| raw.get(key).map(|v| v != "false").unwrap_or(true);
    Config {
        agent_cmd: raw.get("agent.cmd").cloned().filter(|s| !s.is_empty()),
        routes_enabled: flag("routes.enabled"),
        routes_auto: flag("routes.auto"),
        raw,
    }
}

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

    #[test]
    fn parses_flat_subset() {
        let cfg = parse("# radar config\n[agent]\ncmd = \"claude\"\n\n[scan]\nmax = 10\n");
        assert_eq!(cfg.agent_cmd.as_deref(), Some("claude"));
        assert_eq!(cfg.raw.get("scan.max").map(String::as_str), Some("10"));
    }

    #[test]
    fn route_flags_default_on_and_force_disable() {
        assert!(parse("").routes_enabled && parse("").routes_auto);
        let cfg = parse("[routes]\nenabled = false\n");
        assert!(!cfg.routes_enabled);
        let cfg = parse("[routes]\nauto = false\n");
        assert!(cfg.routes_enabled && !cfg.routes_auto);
    }

    #[test]
    fn missing_file_and_junk_lines_degrade() {
        let cfg = parse("just some prose\n[weird\nkey without eq\n");
        assert_eq!(cfg.agent_cmd, None);
        let cfg = Config::load(Path::new("/nonexistent-gean"));
        assert_eq!(cfg.agent_cmd, None);
    }

    #[test]
    fn trailing_comments_do_not_leak_into_values() {
        let cfg =
            parse("[agent]\ncmd = \"runner # retained\" # removed\n[scan]\nmax = 10 # limit\n");
        assert_eq!(cfg.agent_cmd.as_deref(), Some("runner # retained"));
        assert_eq!(cfg.raw.get("scan.max").map(String::as_str), Some("10"));
    }
}