memnite-ingest 0.2.1

Extensible ingestion layer for memnite: the IngestSource plugin seam plus Repo Brain and crumbex graph adapters.
Documentation
use std::fs;
use std::path::Path;

use memnite_core::Scope;

use crate::error::IngestError;
use crate::parse::split_h2_sections;
use crate::source::{IngestSource, IngestedMemory};

/// Adapter for guildgate's Repo Brain. Reads `UBIQUITOUS_LANGUAGE.md` (one memory
/// per glossary term — the `## ` sections AFTER a `## Terms` heading) plus
/// `docs/wiki/ai/CONTRACTS.md` and `CONSTRAINTS.md` (one whole-document memory
/// each). Knows the FORMAT (a data contract), never guildgate's code. Missing
/// files are skipped.
pub struct RepoBrainSource;

impl IngestSource for RepoBrainSource {
    fn name(&self) -> &str {
        "brain"
    }

    fn collect(&self, root: &Path) -> Result<Vec<IngestedMemory>, IngestError> {
        let mut out = Vec::new();

        let ul_path = root.join("UBIQUITOUS_LANGUAGE.md");
        if ul_path.exists() {
            let content = fs::read_to_string(&ul_path)?;
            let sections = split_h2_sections(&content);
            // Terms are the sections after the `## Terms` heading. Without that
            // marker the file isn't in the expected glossary layout -> no terms.
            if let Some(start) = sections
                .iter()
                .position(|(h, _)| h.eq_ignore_ascii_case("Terms"))
            {
                for (heading, body) in sections.into_iter().skip(start + 1) {
                    out.push(IngestedMemory {
                        source_key: format!("ubiquitous-language::{heading}"),
                        title: heading,
                        body,
                        mem_type: "term".to_string(),
                        topic_key: Some("ubiquitous-language".to_string()),
                        scope: Scope::Repo,
                    });
                }
            }
        }

        for (rel, title, mem_type, topic) in [
            (
                "docs/wiki/ai/CONTRACTS.md",
                "CONTRACTS",
                "contract",
                "contracts",
            ),
            (
                "docs/wiki/ai/CONSTRAINTS.md",
                "CONSTRAINTS",
                "constraint",
                "constraints",
            ),
        ] {
            let path = root.join(rel);
            if path.exists() {
                let content = fs::read_to_string(&path)?;
                out.push(IngestedMemory {
                    source_key: topic.to_string(),
                    title: title.to_string(),
                    body: content.trim().to_string(),
                    mem_type: mem_type.to_string(),
                    topic_key: Some(topic.to_string()),
                    scope: Scope::Repo,
                });
            }
        }

        Ok(out)
    }
}