agents-docs-manager 0.1.0

Manage AGENTS.md and Docs markdown indexes for agent-oriented repositories.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocumentMetadata {
    pub title: String,
    pub description: String,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DocumentMetadataError {
    MissingTitle,
    MissingDescription,
    InvalidDescription,
}

impl DocumentMetadataError {
    pub fn message(self) -> &'static str {
        match self {
            Self::MissingTitle => "document must start with a level-one title",
            Self::MissingDescription => {
                "document must include a metadata paragraph after the title"
            }
            Self::InvalidDescription => {
                "document metadata paragraph must match: 本文说明<description>。"
            }
        }
    }
}

pub fn extract_metadata(content: &str) -> Result<DocumentMetadata, DocumentMetadataError> {
    let mut lines = content.lines();
    let title = lines
        .next()
        .and_then(extract_title_from_line)
        .ok_or(DocumentMetadataError::MissingTitle)?;

    let description =
        extract_metadata_paragraph(lines).ok_or(DocumentMetadataError::MissingDescription)?;
    if !is_valid_metadata_description(&description) {
        return Err(DocumentMetadataError::InvalidDescription);
    }

    Ok(DocumentMetadata { title, description })
}

fn extract_title_from_line(line: &str) -> Option<String> {
    let trimmed = line.trim();
    trimmed
        .strip_prefix("# ")
        .map(str::trim)
        .filter(|title| !title.is_empty())
        .map(ToOwned::to_owned)
}

fn extract_metadata_paragraph<'a>(lines: impl IntoIterator<Item = &'a str>) -> Option<String> {
    let mut paragraph = Vec::new();
    let mut in_paragraph = false;

    for line in lines {
        let trimmed = line.trim();
        if trimmed.is_empty() {
            if in_paragraph {
                break;
            }
            continue;
        }

        in_paragraph = true;
        paragraph.push(trimmed);
    }

    if paragraph.is_empty() {
        None
    } else {
        Some(paragraph.join(" "))
    }
}

fn is_valid_metadata_description(description: &str) -> bool {
    description.starts_with("本文说明")
        && description.ends_with('')
        && description
            .trim_start_matches("本文说明")
            .trim_end_matches('')
            .trim()
            .chars()
            .next()
            .is_some()
}

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

    #[test]
    fn extracts_required_title_and_metadata_description() {
        let content = "# 代码规范\n\n本文说明 Rust 代码规范。\n\n## 适用范围\n";
        let metadata = extract_metadata(content).unwrap();
        assert_eq!(metadata.title, "代码规范");
        assert_eq!(metadata.description, "本文说明 Rust 代码规范。");
    }

    #[test]
    fn rejects_freeform_description() {
        let content = "# CODE_STYLE\n\nRust code style conventions.\n";
        let error = extract_metadata(content).unwrap_err();
        assert_eq!(error, DocumentMetadataError::InvalidDescription);
    }
}