rdar 0.6.16

radar - the repository cartographer for AI agents: compiles a repo into tiny committed MAP.md routers, with measured token benchmarks
Documentation
//! Strict frontmatter subset: looks like YAML, is not YAML.
//!
//! Grammar, deliberately tiny: a `---` fence line, then `key: value` or
//! `key: [a, b]` lines (`#`-comment lines ignored), then a closing `---`.
//! No nesting, no anchors, no multi-doc, no quoting rules - rejected by
//! design. Unknown keys are preserved in order (round-trip safe for hand
//! annotations).

/// Parsed frontmatter: ordered key → raw value (list values keep brackets).
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Frontmatter {
    entries: Vec<(String, String)>,
}

impl Frontmatter {
    pub fn new() -> Frontmatter {
        Frontmatter::default()
    }

    pub fn get(&self, key: &str) -> Option<&str> {
        self.entries
            .iter()
            .find(|(k, _)| k == key)
            .map(|(_, v)| v.as_str())
    }

    /// Parse a bracketed list value: `[a, b]` → `["a", "b"]`; `[]` → `[]`.
    pub fn get_list(&self, key: &str) -> Option<Vec<String>> {
        let raw = self.get(key)?.trim();
        let inner = raw.strip_prefix('[')?.strip_suffix(']')?;
        if inner.trim().is_empty() {
            return Some(Vec::new());
        }
        Some(
            inner
                .split(',')
                .map(|s| s.trim().to_string())
                .filter(|s| !s.is_empty())
                .collect(),
        )
    }

    /// Set or replace a key (insertion order preserved for new keys).
    pub fn set(&mut self, key: &str, value: impl Into<String>) {
        let value = value.into();
        if let Some(slot) = self.entries.iter_mut().find(|(k, _)| k == key) {
            slot.1 = value;
        } else {
            self.entries.push((key.to_string(), value));
        }
    }

    pub fn set_list(&mut self, key: &str, values: &[String]) {
        self.set(key, format!("[{}]", values.join(", ")));
    }

    pub fn remove(&mut self, key: &str) {
        self.entries.retain(|(k, _)| k != key);
    }

    pub fn keys(&self) -> impl Iterator<Item = &str> {
        self.entries.iter().map(|(k, _)| k.as_str())
    }

    /// Render including both `---` fences, keys in stored order.
    pub fn render(&self) -> String {
        let mut out = String::from("---\n");
        for (k, v) in &self.entries {
            out.push_str(k);
            out.push_str(": ");
            out.push_str(v);
            out.push('\n');
        }
        out.push_str("---\n");
        out
    }
}

/// Split a document into (frontmatter, body). Returns `None` when the
/// document has no valid frontmatter block (consumers treat the whole text
/// as body - degrade, don't block).
pub fn parse(doc: &str) -> Option<(Frontmatter, &str)> {
    let rest = doc.strip_prefix("---")?;
    let rest = rest.strip_prefix('\r').unwrap_or(rest);
    let rest = rest.strip_prefix('\n')?;
    let mut fm = Frontmatter::new();
    let mut offset = 0usize;
    for line in rest.split_inclusive('\n') {
        let trimmed = line.trim_end_matches(['\r', '\n']);
        if trimmed == "---" {
            let body = &rest[offset + line.len()..];
            return Some((fm, body));
        }
        offset += line.len();
        if trimmed.trim().is_empty() || trimmed.trim_start().starts_with('#') {
            continue;
        }
        // Indented lines mean nesting - not our grammar, reject the block.
        if trimmed.starts_with([' ', '\t']) {
            return None;
        }
        let (key, value) = trimmed.split_once(':')?;
        let key = key.trim();
        let value = value.trim();
        if key.is_empty() || key.contains(char::is_whitespace) || value.is_empty() {
            return None; // not our grammar - reject the whole block
        }
        fm.set(key, value);
    }
    None // unterminated fence
}

/// Byte length of the body with frontmatter excluded.
pub fn body_bytes(doc: &str) -> usize {
    parse(doc).map_or(doc.len(), |(_, body)| body.len())
}

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

    const DOC: &str = "---\nmap: 1\nscope: src/auth\nchildren: [jwt/MAP.md, oauth/MAP.md]\nx-custom: kept\n---\n# body\n";

    #[test]
    fn parses_and_round_trips() {
        let (fm, body) = parse(DOC).expect("parse");
        assert_eq!(fm.get("map"), Some("1"));
        assert_eq!(fm.get("scope"), Some("src/auth"));
        assert_eq!(
            fm.get_list("children"),
            Some(vec!["jwt/MAP.md".to_string(), "oauth/MAP.md".to_string()])
        );
        assert_eq!(fm.get("x-custom"), Some("kept"), "unknown keys preserved");
        assert_eq!(body, "# body\n");
        assert_eq!(fm.render() + body, DOC, "byte-exact round trip");
    }

    #[test]
    fn rejects_non_subset_yaml() {
        assert!(
            parse("---\nnested:\n  a: 1\n---\n").is_none(),
            "nested key rejected"
        );
        assert!(parse("---\nno fence").is_none(), "unterminated");
        assert!(parse("no frontmatter").is_none());
    }

    #[test]
    fn empty_list_and_missing_keys() {
        let (fm, _) = parse("---\nchildren: []\n---\n").expect("parse");
        assert_eq!(fm.get_list("children"), Some(vec![]));
        assert_eq!(fm.get("absent"), None);
        assert_eq!(fm.get_list("absent"), None);
    }

    #[test]
    fn set_and_render_canonical() {
        let mut fm = Frontmatter::new();
        fm.set("map", "1");
        fm.set_list("children", &["a/MAP.md".into()]);
        fm.set("map", "1"); // idempotent overwrite keeps position
        assert_eq!(fm.render(), "---\nmap: 1\nchildren: [a/MAP.md]\n---\n");
    }

    #[test]
    fn body_bytes_excludes_frontmatter() {
        assert_eq!(body_bytes(DOC), "# body\n".len());
        assert_eq!(body_bytes("just body"), "just body".len());
    }

    #[test]
    fn keys_iterate_in_insertion_order() {
        let (fm, _) = parse(DOC).expect("parse");
        let keys: Vec<&str> = fm.keys().collect();
        assert_eq!(keys, vec!["map", "scope", "children", "x-custom"]);
    }
}