malvin 0.2.9

Non-interactive research and coding agent
use std::io::{self, Write};

struct AdviceEntry {
    tag: &'static str,
    body: &'static str,
    description: &'static str,
}

include!(concat!(env!("OUT_DIR"), "/advice_registry.rs"));

fn advice_entry(tag: &str) -> Result<&'static AdviceEntry, String> {
    if !is_valid_advice_tag(tag) {
        return Err(format!(
            "invalid --advice TAG `{tag}`: must be lowercase letters/digits/underscores, start with a letter, at most 32 characters"
        ));
    }
    ADVICE_ENTRIES.iter().find(|e| e.tag == tag).ok_or_else(|| {
        let known: Vec<&str> = ADVICE_ENTRIES.iter().map(|e| e.tag).collect();
        format!("unknown --advice TAG `{tag}` (known: {})", known.join(", "))
    })
}

pub(crate) fn advice_text(tag: &str) -> Result<&'static str, String> {
    advice_entry(tag).map(|e| e.body)
}

fn is_valid_advice_tag(tag: &str) -> bool {
    let mut chars = tag.chars();
    let Some(first) = chars.next() else {
        return false;
    };
    if !first.is_ascii_lowercase() || tag.len() > 32 {
        return false;
    }
    chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
}

fn word_count(s: &str) -> usize {
    s.split_whitespace().count()
}

fn format_advice_list_line(entry: &AdviceEntry) -> String {
    format!("{}: {}", entry.tag, entry.description)
}

const ADVICE_LIST_USAGE: &str = "\
Type `malvin --advice TAG` to see a full document.
TAG: Description";

pub(crate) fn print_advice_list_to_writer(mut out: impl Write) -> Result<(), String> {
    out.write_all(ADVICE_LIST_USAGE.as_bytes())
        .map_err(|e| format!("stdout: {e}"))?;
    out.write_all(b"\n").map_err(|e| format!("stdout: {e}"))?;
    for entry in ADVICE_ENTRIES {
        debug_assert!(
            word_count(entry.description) <= 7,
            "advice description for `{}` exceeds 7 words",
            entry.tag
        );
        let line = format_advice_list_line(entry);
        out.write_all(line.as_bytes())
            .map_err(|e| format!("stdout: {e}"))?;
        out.write_all(b"\n").map_err(|e| format!("stdout: {e}"))?;
    }
    Ok(())
}

pub(crate) fn print_advice_list() -> Result<(), String> {
    print_advice_list_to_writer(io::stdout().lock())
}

pub(crate) fn print_advice_to_writer(tag: &str, mut out: impl Write) -> Result<(), String> {
    let text = advice_text(tag)?;
    out.write_all(text.as_bytes())
        .map_err(|e| format!("stdout: {e}"))?;
    if !text.ends_with('\n') {
        out.write_all(b"\n").map_err(|e| format!("stdout: {e}"))?;
    }
    Ok(())
}

pub(crate) fn print_advice(tag: &str) -> Result<(), String> {
    print_advice_to_writer(tag, io::stdout().lock())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashSet;
    use std::fs;
    use std::path::Path;

    #[test]
    fn doc_design_tag_prints_document_design() {
        let mut buf = Vec::new();
        print_advice_to_writer("doc_design", &mut buf).expect("print");
        let s = String::from_utf8(buf).expect("utf8");
        assert!(s.contains("C.R.A.P."));
        assert!(s.contains("Contrast"));
    }

    #[test]
    fn scholar_tag_prints_scholarly_advice() {
        let mut buf = Vec::new();
        print_advice_to_writer("scholar", &mut buf).expect("print");
        let s = String::from_utf8(buf).expect("utf8");
        assert!(s.contains("XYZ+1"));
        assert!(s.contains("Canonical structure"));
    }

    #[test]
    fn report_tag_prints_report_advice() {
        let mut buf = Vec::new();
        print_advice_to_writer("report", &mut buf).expect("print");
        let s = String::from_utf8(buf).expect("utf8");
        assert!(s.contains("concrete evidence"));
        assert!(s.contains("complete sentences"));
    }

    #[test]
    fn unknown_tag_errors() {
        let err = advice_text("nope").expect_err("unknown");
        assert!(err.contains("unknown"), "{err}");
        assert!(err.contains("doc_design"), "{err}");
        assert!(err.contains("report"), "{err}");
        assert!(err.contains("scholar"), "{err}");
    }

    #[test]
    fn invalid_tag_shape_errors() {
        assert!(advice_text("Bad").is_err());
        assert!(advice_text("1abc").is_err());
        assert!(advice_text("").is_err());
        assert!(advice_text("a-b").is_err());
        assert!(advice_text(&"a".repeat(33)).is_err());
    }

    #[test]
    fn valid_tag_shape_accepted_for_lookup() {
        assert!(is_valid_advice_tag("design"));
        assert!(is_valid_advice_tag("doc_design"));
        assert!(is_valid_advice_tag("a1"));
        assert!(is_valid_advice_tag("abcdefg"));
        assert!(is_valid_advice_tag(&"a".repeat(32)));
    }

    #[test]
    fn advice_list_prints_tag_colon_description_lines() {
        let mut buf = Vec::new();
        print_advice_list_to_writer(&mut buf).expect("list");
        let s = String::from_utf8(buf).expect("utf8");
        let lines: Vec<&str> = s.lines().collect();
        assert_eq!(
            lines[0], "Type `malvin --advice TAG` to see a full document.",
            "{s}"
        );
        assert_eq!(lines[1], "TAG: Description", "{s}");
        assert!(!s.contains("List format"), "{s}");
        let expected: Vec<String> = ADVICE_ENTRIES
            .iter()
            .map(|e| format!("{}: {}", e.tag, e.description))
            .collect();
        assert_eq!(lines[2..], expected[..], "{s}");
    }

    #[test]
    fn all_advice_descriptions_are_at_most_seven_words() {
        for entry in ADVICE_ENTRIES {
            let n = word_count(entry.description);
            assert!(
                n <= 7,
                "tag `{}` description has {n} words: {}",
                entry.tag,
                entry.description
            );
        }
    }

    #[test]
    fn advice_descriptions_come_from_first_line_without_prefix() {
        let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("default_prompts/advice");
        for entry in ADVICE_ENTRIES {
            let path = dir.join(format!("{}.md", entry.tag));
            let content = fs::read_to_string(&path).expect("read advice md");
            let first = content.lines().next().expect("non-empty advice md");
            let value = first
                .strip_prefix("description:")
                .unwrap_or_else(|| panic!("{}: first line must be `description: ...`", entry.tag))
                .trim();
            assert_eq!(
                entry.description, value,
                "registry description must match first-line value for {}",
                entry.tag
            );
            assert!(
                !entry.description.starts_with("description:"),
                "description must not retain the `description:` prefix: {}",
                entry.description
            );
        }
    }

    #[test]
    fn every_advice_md_stem_is_registered_as_tag() {
        let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("default_prompts/advice");
        let mut stems = HashSet::new();
        for entry in fs::read_dir(&dir).expect("read advice dir") {
            let entry = entry.expect("dir entry");
            let path = entry.path();
            if path.extension().and_then(|e| e.to_str()) != Some("md") {
                continue;
            }
            let stem = path
                .file_stem()
                .and_then(|s| s.to_str())
                .expect("utf8 stem")
                .to_string();
            assert!(
                is_valid_advice_tag(&stem),
                "advice file stem `{stem}` must be a valid --advice TAG"
            );
            stems.insert(stem);
        }
        let tags: HashSet<&str> = ADVICE_ENTRIES.iter().map(|e| e.tag).collect();
        assert_eq!(
            stems,
            tags.iter().map(|t| (*t).to_string()).collect(),
            "ADVICE_ENTRIES tags must equal advice/*.md stems"
        );
        for entry in ADVICE_ENTRIES {
            assert!(stems.contains(entry.tag));
        }
    }
}