cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
use crate::domain::model::record_ref::{DecisionRecordRef, IssueRef};
use crate::domain::usecases::decision_record::{resolve_decision_record, DecisionRecordRepository};
use crate::domain::usecases::issue::{resolve_issue, IssueRepository, Resolved};
use crate::infra::driving::cli::errors::{die1, CliError};
use crate::infra::driving::cli::OutputFormat;

/// Parse a decision record ID from the CLI argument.
///
/// Accepts both the full prefixed form (`ADR-0001`) and a bare integer (`1` or
/// `0001`).  When a bare integer is given, it is reconstructed using
/// `id_prefix` (e.g. `"ADR-"`) or falls back to `"ADR-"` when unconfigured.
pub(super) fn parse_dr_id(raw: &str, id_prefix: Option<&str>) -> anyhow::Result<DecisionRecordRef> {
    if let Ok(r) = raw.parse::<DecisionRecordRef>() {
        return Ok(r);
    }
    if let Ok(n) = raw.parse::<u32>() {
        if n > 0 {
            let prefix = id_prefix.unwrap_or("ADR-").trim_end_matches('-');
            return DecisionRecordRef::new(format!("{prefix}-{n:04}"));
        }
    }
    anyhow::bail!("invalid entity ref '{raw}': not a valid prefixed ID or bare integer")
}

/// Parse an issue ID from the CLI argument.
///
/// Accepts both the full prefixed form (`ISSUE-0042`) and a bare integer (`42`
/// or `0042`).  When a bare integer is given, it is reconstructed using
/// `id_prefix` (e.g. `"ISSUE-"`) or falls back to `"ISSUE-"` when unconfigured.
pub(super) fn parse_issue_id(raw: &str, id_prefix: Option<&str>) -> anyhow::Result<IssueRef> {
    if let Ok(r) = raw.parse::<IssueRef>() {
        return Ok(r);
    }
    if let Ok(n) = raw.parse::<u32>() {
        if n > 0 {
            let prefix = id_prefix.unwrap_or("ISSUE-").trim_end_matches('-');
            return IssueRef::new(format!("{prefix}-{n:04}"));
        }
    }
    anyhow::bail!("invalid entity ref '{raw}': not a valid prefixed ID or bare integer")
}

/// Resolve a user-supplied issue ID to its canonical `IssueRef`, using the
/// repository to honour ADR-0022 prefix matching and alias forwarding.
///
/// Resolution path:
/// 1. Exact canonical id or unique TSID prefix via [`IssueRepository::resolve`].
/// 2. Alias scan (via the same `resolve` call).
/// 3. Fallback: parse as a bare integer / shape-only ref via [`parse_issue_id`]
///    so error messages still name the bad shape rather than just "not found".
///
/// Calls [`die1`] on ambiguous prefixes or invalid input.
pub(super) fn resolve_issue_id(
    repo: &dyn IssueRepository,
    raw: &str,
    id_prefix: Option<&str>,
    output_fmt: OutputFormat,
) -> IssueRef {
    match resolve_issue(repo, raw) {
        Ok(Resolved::Found(issue)) => issue.id,
        Ok(Resolved::AmbiguousPrefix { matches }) => {
            let suffix = raw.rsplit_once('-').map(|(_, s)| s).unwrap_or(raw);
            die1(
                CliError::new(format!(
                    "ambiguous prefix '{}' matches: {}. Disambiguate with a longer prefix.",
                    suffix,
                    matches.join(", ")
                ))
                .kind("validation"),
                output_fmt,
            );
        }
        Ok(Resolved::NotFound) => parse_issue_id(raw, id_prefix).unwrap_or_else(|e| {
            die1(
                CliError::new(format!("invalid issue ID '{raw}': {e}")).kind("validation"),
                output_fmt,
            );
        }),
        Err(e) => die1(CliError::new(e.to_string()).kind("io"), output_fmt),
    }
}

/// Resolve a user-supplied decision record ID. See [`resolve_issue_id`].
pub(super) fn resolve_dr_id(
    repo: &dyn DecisionRecordRepository,
    raw: &str,
    id_prefix: Option<&str>,
    output_fmt: OutputFormat,
) -> DecisionRecordRef {
    match resolve_decision_record(repo, raw) {
        Ok(Resolved::Found(record)) => record.id,
        Ok(Resolved::AmbiguousPrefix { matches }) => {
            let suffix = raw.rsplit_once('-').map(|(_, s)| s).unwrap_or(raw);
            die1(
                CliError::new(format!(
                    "ambiguous prefix '{}' matches: {}. Disambiguate with a longer prefix.",
                    suffix,
                    matches.join(", ")
                ))
                .kind("validation"),
                output_fmt,
            );
        }
        Ok(Resolved::NotFound) => parse_dr_id(raw, id_prefix).unwrap_or_else(|e| {
            die1(
                CliError::new(format!("invalid record ID '{raw}': {e}")).kind("validation"),
                output_fmt,
            );
        }),
        Err(e) => die1(CliError::new(e.to_string()).kind("io"), output_fmt),
    }
}