cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
use crate::domain::model::body::Body;
use crate::domain::model::description::Description;
use crate::domain::model::entry_locator::EntryLocator;
use crate::domain::model::entry_origin::EntryOrigin;
use crate::domain::model::record_kind::RecordKind;
use crate::domain::model::record_ref::DecisionRecordRef;
use crate::domain::model::relates::Relates;
use crate::domain::model::tag_list::TagList;
use crate::domain::model::temporal::iso_date::IsoDate;
use crate::domain::model::title::Title;

use super::{DrStatus, EventLog, RecordLinks};

/// A generic decision record (ADR, DDR, GDDR, …).
///
/// The `kind` field identifies the record type as configured in `cartulary.toml`
/// (e.g. `"adr"`, `"ddr"`, `"gddr"`). It is injected by the repository from
/// the directory context — the file itself does not store the kind.
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct DecisionRecord {
    pub id: DecisionRecordRef,
    pub kind: RecordKind,
    pub title: Title,
    /// Optional one-line summary used as `<meta name="description">` in the
    /// generated site and as a fallback for cross-reference link previews.
    pub description: Option<Description>,
    pub(crate) status: DrStatus,
    pub date: IsoDate,
    /// Free-form labels (simple `backend` or structured `area:backend`) used
    /// for filtering. ISSUE-0110. Field is `pub(crate)` to enforce the
    /// `with_tags` builder per ADR-00000000218SQ; reading is still free.
    pub(crate) tags: TagList,
    /// Forwarding identifiers — strings that resolve to this record when
    /// used in place of `id`. Populated by the v3→v4 migration (ADR-0022)
    /// to keep historical `ADR-NNNN`/`DDR-NNNN` references working.
    pub aliases: Vec<String>,
    pub(crate) content: Body,
    pub(crate) events: EventLog,
    pub(crate) links: RecordLinks,
    /// Cross-kind "see also" pointers, separate from typed `links:`.
    /// See ISSUE-018P03NSC7VNQ and DDR-018QWJVHRH35B.
    pub(crate) relates: Relates,
    /// Which workspace side this record was loaded from — see
    /// [`crate::domain::model::issue::Issue::origin`].
    pub origin: EntryOrigin,
    /// Where this record was loaded from — see
    /// [`crate::domain::model::issue::Issue::location`].
    pub location: EntryLocator,
}

impl DecisionRecord {
    /// Return a new record identical to `self` but with `tags` replaced.
    /// Pure transform aligned with ADR-00000000218SQ — does not mutate.
    pub fn with_tags(self, tags: TagList) -> Self {
        Self { tags, ..self }
    }

    /// Return a new record identical to `self` but with `content` replaced.
    /// Pure transform aligned with ADR-00000000218SQ — does not mutate.
    pub fn with_content(self, content: Body) -> Self {
        Self { content, ..self }
    }

    /// Return a new record identical to `self` but with `title` replaced.
    /// Pure transform aligned with ADR-00000000218SQ — does not mutate.
    pub fn with_title(self, title: Title) -> Self {
        Self { title, ..self }
    }

    /// Return a new record identical to `self` but with `status` replaced.
    /// Pure transform aligned with ADR-00000000218SQ — does not mutate.
    pub fn with_status(self, status: DrStatus) -> Self {
        Self { status, ..self }
    }

    /// Return a new record identical to `self` but with `events` replaced.
    /// Pure transform aligned with ADR-00000000218SQ — does not mutate.
    pub fn with_events(self, events: EventLog) -> Self {
        Self { events, ..self }
    }

    /// Return a new record identical to `self` but with `links` replaced.
    /// Pure transform aligned with ADR-00000000218SQ — does not mutate.
    pub fn with_links(self, links: RecordLinks) -> Self {
        Self { links, ..self }
    }

    /// Return a new record identical to `self` but with `relates` replaced.
    /// Pure transform aligned with ADR-00000000218SQ — does not mutate.
    pub fn with_relates(self, relates: Relates) -> Self {
        Self { relates, ..self }
    }

    pub fn relates(&self) -> &Relates {
        &self.relates
    }
}

#[cfg(test)]
pub mod strategy {
    use super::DecisionRecord;
    use crate::domain::model::body::strategy::arb_body;
    use crate::domain::model::decision_record::dr_status::strategy::dr_status;
    use crate::domain::model::decision_record::record_link::strategy::record_links;
    use crate::domain::model::event::strategy::event_log;
    use crate::domain::model::record_kind::strategy::record_kind;
    use crate::domain::model::record_ref::strategy::decision_record_ref;
    use crate::domain::model::relates::strategy::relates;
    use crate::domain::model::tag_list::strategy::tag_list;
    use crate::domain::model::temporal::iso_date::strategy::iso_date;
    use crate::domain::model::title::strategy::arb_title;
    use proptest::prelude::*;

    /// Generate an arbitrary `DecisionRecord` from leaf strategies.
    pub fn decision_record() -> impl Strategy<Value = DecisionRecord> {
        let head = (
            decision_record_ref(),
            record_kind(),
            arb_title(),
            proptest::option::of(crate::domain::model::description::strategy::arb_description()),
            dr_status(),
            iso_date(),
        );
        let tail = (
            tag_list(),
            proptest::collection::vec("[A-Z]+-[0-9]{4}", 0..3),
            arb_body(),
            event_log(),
            record_links(),
            relates(),
        );
        (head, tail).prop_map(
            |(
                (id, kind, title, description, status, date),
                (tags, aliases, content, events, links, relates),
            )| DecisionRecord {
                id,
                kind,
                title,
                description,
                status,
                date,
                tags,
                aliases,
                content,
                events,
                links,
                relates,
                origin: crate::domain::model::entry_origin::EntryOrigin::Local,
                location: crate::domain::model::entry_locator::EntryLocator::default(),
            },
        )
    }
}

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

    proptest! {
        #[test]
        fn clone_round_trips(r in strategy::decision_record()) {
            prop_assert_eq!(r.clone(), r);
        }
    }
}