cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
use crate::domain::model::decision_record::DecisionRecord;
use crate::domain::model::record_ref::DecisionRecordRef;
use crate::domain::usecases::issue::resolve::split_for_prefix_match;
use crate::domain::usecases::issue::Resolved;

use super::repository::DecisionRecordRepository;

/// Resolve a free-form reference to a record: the canonical `id:` first,
/// then any string in `aliases:` (ADR-0022). Returns `Ok(None)` only if
/// nothing matches.
pub fn find_decision_record_by_id_or_alias(
    repo: &dyn DecisionRecordRepository,
    raw: &str,
) -> anyhow::Result<Option<DecisionRecord>> {
    match resolve_decision_record(repo, raw)? {
        Resolved::Found(record) => Ok(Some(record)),
        Resolved::NotFound | Resolved::AmbiguousPrefix { .. } => Ok(None),
    }
}

/// Resolve a free-form reference into a typed outcome (ADR-0022 phase 4).
/// Mirrors [`crate::domain::usecases::issue::resolve_issue`]; see that
/// function for the policy.
pub fn resolve_decision_record(
    repo: &dyn DecisionRecordRepository,
    raw: &str,
) -> anyhow::Result<Resolved<DecisionRecord>> {
    if let Ok(id) = raw.parse::<DecisionRecordRef>() {
        if let Some(found) = repo.find_by_id(&id)? {
            return Ok(Resolved::Found(found));
        }
    }

    let records = repo.list()?;
    if let Some((prefix, suffix)) = split_for_prefix_match(raw) {
        if suffix.len() >= 3 {
            let needle = format!("{prefix}-{}", suffix.to_ascii_uppercase());
            let mut hits = Vec::new();
            let mut hit_records = Vec::new();
            for record in &records {
                let id_str = record.id.as_str();
                if id_str.len() != needle.len() && id_str.to_ascii_uppercase().starts_with(&needle)
                {
                    hits.push(id_str.to_string());
                    hit_records.push(record.clone());
                }
            }
            match hits.len() {
                0 => {}
                1 => return Ok(Resolved::Found(hit_records.into_iter().next().unwrap())),
                _ => return Ok(Resolved::AmbiguousPrefix { matches: hits }),
            }
        }
    }

    for record in records {
        if record.aliases.iter().any(|a| a == raw) {
            return Ok(Resolved::Found(record));
        }
    }
    Ok(Resolved::NotFound)
}