memnite-ingest 0.2.1

Extensible ingestion layer for memnite: the IngestSource plugin seam plus Repo Brain and crumbex graph adapters.
Documentation
/// Lowercase ASCII; keep alphanumerics; collapse every other run of characters
/// into a single `-`; trim leading/trailing `-`. Deterministic and dependency-free.
/// Note: distinct inputs can collapse to the same slug (e.g. "A B", "A-B", "A_B"
/// all -> "a-b"); callers relying on slugs as identity keys must keep source keys
/// slug-distinct.
/// Non-ASCII chars are silently skipped (NFD combining marks are also skipped,
/// so NFD-encoded accented letters yield their ASCII base character).
pub fn slugify(s: &str) -> String {
    let mut out = String::new();
    let mut prev_dash = false;
    for ch in s.chars() {
        if ch.is_ascii_alphanumeric() {
            out.push(ch.to_ascii_lowercase());
            prev_dash = false;
        } else if ch.is_ascii() && !prev_dash {
            // ASCII non-alphanumeric (space, punctuation, etc.) → separator dash
            out.push('-');
            prev_dash = true;
        }
        // non-ASCII chars (precomposed accented letters, combining marks) are skipped
    }
    out.trim_matches('-').to_string()
}

/// Split markdown into its level-2 (`## `) sections. Returns `(heading, body)`
/// for each `## ` line that starts at column 0; `body` is everything up to the
/// next such heading, trimmed. Content before the first heading is ignored.
/// Indented `## ` (e.g. inside a code block) is NOT treated as a heading.
pub fn split_h2_sections(content: &str) -> Vec<(String, String)> {
    let mut sections: Vec<(String, String)> = Vec::new();
    let mut cur: Option<(String, Vec<&str>)> = None;
    for line in content.lines() {
        if let Some(rest) = line.strip_prefix("## ") {
            if let Some((h, body)) = cur.take() {
                sections.push((h, body.join("\n").trim().to_string()));
            }
            cur = Some((rest.trim().to_string(), Vec::new()));
        } else if let Some((_, body)) = cur.as_mut() {
            body.push(line);
        }
    }
    if let Some((h, body)) = cur {
        sections.push((h, body.join("\n").trim().to_string()));
    }
    sections
}