koala-core 1.0.4

Shared types, invariant evaluator, and primitives for the koala framework.
Documentation
//! Wiki helpers shared across koala crates.
//!
//! Markdown parsing here is intentionally minimal — every routine is a
//! grep-equivalent, fast, and tolerant of unrecognised structure. Anything
//! more clever (LLM extraction, full markdown AST) belongs in a dedicated
//! parser crate, not here. See ADR-0014.

use std::collections::HashMap;

/// Returns the YAML frontmatter body (between the leading `---` fences),
/// or `None` if the document has no frontmatter or the closing fence is
/// missing. Accepts both LF and CRLF line endings — silent miss on a
/// CRLF-saved file would defeat every check that branches on frontmatter
/// (`status: superseded`, ADR dormancy, etc.), which is exactly the
/// failure mode drift detection exists to prevent.
pub fn extract_frontmatter(content: &str) -> Option<&str> {
    let rest = content
        .strip_prefix("---\r\n")
        .or_else(|| content.strip_prefix("---\n"))?;
    // `find("\n---")` finds the closing fence under both LF and CRLF
    // (the latter contains `\n---` as a substring). The optional `\r`
    // before `\n---` ends up at the tail of the returned slice; the
    // `parse_yaml_frontmatter` line walker uses `str::lines()`, which
    // already strips trailing `\r`.
    let end = rest.find("\n---")?;
    Some(&rest[..end])
}

/// Parses a flat YAML frontmatter block of `key: value` pairs into a map.
///
/// Skips blank / comment lines and lines whose key is indented (nested under
/// a parent we don't model). Quotes around values are not stripped.
pub fn parse_yaml_frontmatter(text: &str) -> HashMap<String, String> {
    let mut out = HashMap::new();
    for line in text.lines() {
        let trimmed = line.trim();
        if trimmed.is_empty() || trimmed.starts_with('#') {
            continue;
        }
        let Some((k, v)) = line.split_once(':') else {
            continue;
        };
        if k.starts_with(' ') || k.starts_with('\t') {
            continue;
        }
        out.insert(k.trim().to_string(), v.trim().to_string());
    }
    out
}

/// Returns the body of a `## <heading>` section (lines after the heading,
/// up to but not including the next top-level `## ` heading or EOF).
///
/// Heading match is exact on `## <heading>` — trailing parenthetical
/// qualifiers like `## Acceptance criteria(机械可验证)` are matched via
/// prefix; pass `prefix_match = true` to enable that.
pub fn section_body<'a>(content: &'a str, heading: &str, prefix_match: bool) -> Option<&'a str> {
    let target = format!("## {heading}");
    let mut start: Option<usize> = None;
    let mut end: Option<usize> = None;
    let mut idx = 0usize;
    for line in content.split_inclusive('\n') {
        let trimmed = line.trim_end_matches('\n');
        if start.is_none() {
            let hit = if prefix_match {
                trimmed.starts_with(&target)
            } else {
                trimmed == target
            };
            if hit {
                start = Some(idx + line.len());
            }
        } else if trimmed.starts_with("## ") {
            end = Some(idx);
            break;
        }
        idx += line.len();
    }
    let s = start?;
    Some(&content[s..end.unwrap_or(content.len())])
}

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

    #[test]
    fn extract_frontmatter_basic() {
        let doc = "---\nid: 0001\ntitle: foo\n---\n\nbody\n";
        let fm = extract_frontmatter(doc).unwrap();
        let map = parse_yaml_frontmatter(fm);
        assert_eq!(map.get("id").map(String::as_str), Some("0001"));
        assert_eq!(map.get("title").map(String::as_str), Some("foo"));
    }

    #[test]
    fn extract_frontmatter_missing() {
        assert!(extract_frontmatter("no frontmatter\n").is_none());
        assert!(extract_frontmatter("---\nunterminated\n").is_none());
    }

    #[test]
    fn extract_frontmatter_crlf() {
        let doc = "---\r\nid: 0001\r\ntitle: foo\r\nstatus: accepted\r\n---\r\n\r\nbody\r\n";
        let fm = extract_frontmatter(doc).expect("CRLF frontmatter must parse");
        let map = parse_yaml_frontmatter(fm);
        assert_eq!(map.get("id").map(String::as_str), Some("0001"));
        assert_eq!(map.get("status").map(String::as_str), Some("accepted"));
    }

    #[test]
    fn section_body_extracts_until_next_h2() {
        let doc = "# Title\n\n## A\nalpha\n\n## B\nbeta\n";
        let body = section_body(doc, "A", false).unwrap();
        assert_eq!(body.trim(), "alpha");
    }

    #[test]
    fn section_body_prefix_match() {
        let doc = "## Acceptance criteria(机械可验证)\n- [x] foo\n\n## Next\n";
        let body = section_body(doc, "Acceptance criteria", true).unwrap();
        assert!(body.contains("- [x] foo"));
    }
}