cartulary 0.3.0-alpha.1

The knowledge layer of your project — decisions, issues, docs, all in one place.
Documentation
//! Hard load-time failures reported by an [`EntryDefectScanner`].
//!
//! Distinct from semantic warnings on a successfully parsed entry,
//! which stay on the repository surface. A `LoadDefect` means the
//! adapter could not produce a valid domain value from the source.

/// Closed set of load-time failures the domain knows how to talk
/// about. Adapters translate their own error vocabulary
/// (`io::Error`, `serde_yaml::Error`, …) into one of these variants.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum LoadDefect {
    /// Source could not be read at all (missing file, permission denied,
    /// transport error, …). `reason` is an adapter-formatted human message.
    SourceUnreadable { reason: String },
    /// Frontmatter was present but unparseable.
    InvalidFrontmatter { reason: String },
    /// Frontmatter was readable but did not carry an `id:` field.
    MissingId,
    /// The entry's `id:` did not start with the corpus's configured
    /// `id_prefix`. Carries both sides for the error message.
    IdPrefixMismatch { expected: String, found: String },
    /// The entry's `status:` is not part of the corpus's allowed set.
    InvalidStatus { value: String },
    /// The entry parsed cleanly but its `events.jsonl` sibling is absent,
    /// breaking the v7 structural contract.
    MissingEventsLog,
    /// The frontmatter `status:` field does not match the journal's
    /// terminal projection; either the frontmatter or the journal is stale.
    StatusJournalMismatch {
        frontmatter: String,
        terminal: String,
    },
}

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

    pub fn arb_load_defect() -> impl Strategy<Value = LoadDefect> {
        prop_oneof![
            "[a-z0-9 ]{1,40}".prop_map(|reason| LoadDefect::SourceUnreadable { reason }),
            "[a-z0-9 ]{1,40}".prop_map(|reason| LoadDefect::InvalidFrontmatter { reason }),
            Just(LoadDefect::MissingId),
            ("[A-Z]{2,5}-", "[A-Za-z]{2,5}-")
                .prop_map(|(expected, found)| { LoadDefect::IdPrefixMismatch { expected, found } }),
            "[a-z]{1,20}".prop_map(|value| LoadDefect::InvalidStatus { value }),
            Just(LoadDefect::MissingEventsLog),
        ]
    }
}

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

    #[test]
    fn variants_are_distinguishable_by_equality() {
        assert_ne!(
            LoadDefect::MissingId,
            LoadDefect::InvalidStatus { value: "x".into() }
        );
        assert_ne!(
            LoadDefect::SourceUnreadable { reason: "a".into() },
            LoadDefect::SourceUnreadable { reason: "b".into() },
        );
    }

    #[test]
    fn equality_is_value_based() {
        assert_eq!(
            LoadDefect::IdPrefixMismatch {
                expected: "ADR-".into(),
                found: "DDR-".into(),
            },
            LoadDefect::IdPrefixMismatch {
                expected: "ADR-".into(),
                found: "DDR-".into(),
            },
        );
    }

    #[test]
    fn hash_groups_equal_values() {
        use std::collections::HashSet;
        let mut set = HashSet::new();
        set.insert(LoadDefect::MissingId);
        set.insert(LoadDefect::MissingId);
        assert_eq!(set.len(), 1);
    }
}