data_modelling_sdk/models/
decision.rs

1//! Decision model for MADR-compliant decision records
2//!
3//! Implements the Data Decision Log (DDL) feature for tracking architectural
4//! and data-related decisions using the MADR (Markdown Any Decision Records)
5//! template format.
6//!
7//! ## File Format
8//!
9//! Decision records are stored as `.madr.yaml` files following the naming convention:
10//! `{workspace}_{domain}_adr-{number}.madr.yaml`
11//!
12//! ## Example
13//!
14//! ```yaml
15//! id: 550e8400-e29b-41d4-a716-446655440000
16//! number: 1
17//! title: "Use ODCS v3.1.0 for all data contracts"
18//! status: accepted
19//! category: datadesign
20//! domain: sales
21//! date: 2026-01-07T10:00:00Z
22//! deciders:
23//!   - data-architecture@company.com
24//! context: |
25//!   We need a standard format for defining data contracts.
26//! decision: |
27//!   We will adopt ODCS v3.1.0 as the standard format.
28//! consequences: |
29//!   Positive: Consistent contracts across domains
30//! ```
31
32use chrono::{DateTime, Utc};
33use serde::{Deserialize, Serialize};
34use uuid::Uuid;
35
36use super::Tag;
37
38/// Decision status in lifecycle
39///
40/// Decisions follow a lifecycle: Proposed → Accepted → [Deprecated | Superseded]
41#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
42#[serde(rename_all = "lowercase")]
43pub enum DecisionStatus {
44    /// Decision has been proposed but not yet accepted
45    #[default]
46    Proposed,
47    /// Decision has been accepted and is in effect
48    Accepted,
49    /// Decision is no longer valid but not replaced
50    Deprecated,
51    /// Decision has been replaced by another decision
52    Superseded,
53}
54
55impl std::fmt::Display for DecisionStatus {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        match self {
58            DecisionStatus::Proposed => write!(f, "Proposed"),
59            DecisionStatus::Accepted => write!(f, "Accepted"),
60            DecisionStatus::Deprecated => write!(f, "Deprecated"),
61            DecisionStatus::Superseded => write!(f, "Superseded"),
62        }
63    }
64}
65
66/// Decision category
67///
68/// Categories help organize decisions by their domain of impact.
69#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
70#[serde(rename_all = "lowercase")]
71pub enum DecisionCategory {
72    /// System architecture decisions
73    #[default]
74    Architecture,
75    /// Data design and modeling decisions
76    DataDesign,
77    /// Workflow and process decisions
78    Workflow,
79    /// Data model structure decisions
80    Model,
81    /// Data governance decisions
82    Governance,
83    /// Security-related decisions
84    Security,
85    /// Performance optimization decisions
86    Performance,
87    /// Compliance and regulatory decisions
88    Compliance,
89    /// Infrastructure decisions
90    Infrastructure,
91    /// Tooling and technology choices
92    Tooling,
93}
94
95impl std::fmt::Display for DecisionCategory {
96    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
97        match self {
98            DecisionCategory::Architecture => write!(f, "Architecture"),
99            DecisionCategory::DataDesign => write!(f, "Data Design"),
100            DecisionCategory::Workflow => write!(f, "Workflow"),
101            DecisionCategory::Model => write!(f, "Model"),
102            DecisionCategory::Governance => write!(f, "Governance"),
103            DecisionCategory::Security => write!(f, "Security"),
104            DecisionCategory::Performance => write!(f, "Performance"),
105            DecisionCategory::Compliance => write!(f, "Compliance"),
106            DecisionCategory::Infrastructure => write!(f, "Infrastructure"),
107            DecisionCategory::Tooling => write!(f, "Tooling"),
108        }
109    }
110}
111
112/// Priority level for decision drivers
113#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
114#[serde(rename_all = "lowercase")]
115pub enum DriverPriority {
116    High,
117    #[default]
118    Medium,
119    Low,
120}
121
122/// Driver/reason for the decision
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124pub struct DecisionDriver {
125    /// Description of why this is a driver
126    pub description: String,
127    /// Priority of this driver
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub priority: Option<DriverPriority>,
130}
131
132impl DecisionDriver {
133    /// Create a new decision driver
134    pub fn new(description: impl Into<String>) -> Self {
135        Self {
136            description: description.into(),
137            priority: None,
138        }
139    }
140
141    /// Create a decision driver with priority
142    pub fn with_priority(description: impl Into<String>, priority: DriverPriority) -> Self {
143        Self {
144            description: description.into(),
145            priority: Some(priority),
146        }
147    }
148}
149
150/// Option considered during decision making
151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
152pub struct DecisionOption {
153    /// Name of the option
154    pub name: String,
155    /// Description of the option
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub description: Option<String>,
158    /// Advantages of this option
159    #[serde(default, skip_serializing_if = "Vec::is_empty")]
160    pub pros: Vec<String>,
161    /// Disadvantages of this option
162    #[serde(default, skip_serializing_if = "Vec::is_empty")]
163    pub cons: Vec<String>,
164    /// Whether this option was selected
165    pub selected: bool,
166}
167
168impl DecisionOption {
169    /// Create a new decision option
170    pub fn new(name: impl Into<String>, selected: bool) -> Self {
171        Self {
172            name: name.into(),
173            description: None,
174            pros: Vec::new(),
175            cons: Vec::new(),
176            selected,
177        }
178    }
179
180    /// Create a decision option with full details
181    pub fn with_details(
182        name: impl Into<String>,
183        description: impl Into<String>,
184        pros: Vec<String>,
185        cons: Vec<String>,
186        selected: bool,
187    ) -> Self {
188        Self {
189            name: name.into(),
190            description: Some(description.into()),
191            pros,
192            cons,
193            selected,
194        }
195    }
196}
197
198/// Relationship type between a decision and an asset
199#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
200#[serde(rename_all = "lowercase")]
201pub enum AssetRelationship {
202    /// Decision affects this asset
203    Affects,
204    /// Decision is implemented by this asset
205    Implements,
206    /// Decision deprecates this asset
207    Deprecates,
208}
209
210/// Link to an asset (table, relationship, product, etc.)
211#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
212pub struct AssetLink {
213    /// Type of asset (odcs, odps, cads, relationship)
214    pub asset_type: String,
215    /// UUID of the linked asset
216    pub asset_id: Uuid,
217    /// Name of the linked asset
218    pub asset_name: String,
219    /// Relationship between decision and asset
220    #[serde(skip_serializing_if = "Option::is_none")]
221    pub relationship: Option<AssetRelationship>,
222}
223
224impl AssetLink {
225    /// Create a new asset link
226    pub fn new(
227        asset_type: impl Into<String>,
228        asset_id: Uuid,
229        asset_name: impl Into<String>,
230    ) -> Self {
231        Self {
232            asset_type: asset_type.into(),
233            asset_id,
234            asset_name: asset_name.into(),
235            relationship: None,
236        }
237    }
238
239    /// Create an asset link with relationship
240    pub fn with_relationship(
241        asset_type: impl Into<String>,
242        asset_id: Uuid,
243        asset_name: impl Into<String>,
244        relationship: AssetRelationship,
245    ) -> Self {
246        Self {
247            asset_type: asset_type.into(),
248            asset_id,
249            asset_name: asset_name.into(),
250            relationship: Some(relationship),
251        }
252    }
253}
254
255/// Compliance assessment for the decision
256#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
257pub struct ComplianceAssessment {
258    /// Impact on regulatory requirements
259    #[serde(skip_serializing_if = "Option::is_none")]
260    pub regulatory_impact: Option<String>,
261    /// Privacy impact assessment
262    #[serde(skip_serializing_if = "Option::is_none")]
263    pub privacy_assessment: Option<String>,
264    /// Security impact assessment
265    #[serde(skip_serializing_if = "Option::is_none")]
266    pub security_assessment: Option<String>,
267    /// Applicable compliance frameworks (GDPR, SOC2, HIPAA, etc.)
268    #[serde(default, skip_serializing_if = "Vec::is_empty")]
269    pub frameworks: Vec<String>,
270}
271
272impl ComplianceAssessment {
273    /// Check if the assessment is empty
274    pub fn is_empty(&self) -> bool {
275        self.regulatory_impact.is_none()
276            && self.privacy_assessment.is_none()
277            && self.security_assessment.is_none()
278            && self.frameworks.is_empty()
279    }
280}
281
282/// Contact details for decision ownership
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
284pub struct DecisionContact {
285    /// Email address
286    #[serde(skip_serializing_if = "Option::is_none")]
287    pub email: Option<String>,
288    /// Contact name
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub name: Option<String>,
291    /// Role or team
292    #[serde(skip_serializing_if = "Option::is_none")]
293    pub role: Option<String>,
294}
295
296/// MADR-compliant Decision Record
297///
298/// Represents an architectural or data decision following the MADR template.
299#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
300pub struct Decision {
301    /// Unique identifier for the decision
302    pub id: Uuid,
303    /// Sequential decision number (ADR-0001, ADR-0002, etc.)
304    pub number: u32,
305    /// Short title describing the decision
306    pub title: String,
307    /// Current status of the decision
308    pub status: DecisionStatus,
309    /// Category of the decision
310    pub category: DecisionCategory,
311    /// Domain this decision belongs to (optional)
312    #[serde(skip_serializing_if = "Option::is_none")]
313    pub domain: Option<String>,
314
315    // MADR template fields
316    /// Date the decision was made
317    pub date: DateTime<Utc>,
318    /// People or teams who made the decision
319    #[serde(default, skip_serializing_if = "Vec::is_empty")]
320    pub deciders: Vec<String>,
321    /// Problem statement and context for the decision
322    pub context: String,
323    /// Reasons driving this decision
324    #[serde(default, skip_serializing_if = "Vec::is_empty")]
325    pub drivers: Vec<DecisionDriver>,
326    /// Options that were considered
327    #[serde(default, skip_serializing_if = "Vec::is_empty")]
328    pub options: Vec<DecisionOption>,
329    /// The decision that was made
330    pub decision: String,
331    /// Positive and negative consequences of the decision
332    #[serde(skip_serializing_if = "Option::is_none")]
333    pub consequences: Option<String>,
334
335    // Linking
336    /// Assets affected by this decision
337    #[serde(default, skip_serializing_if = "Vec::is_empty")]
338    pub linked_assets: Vec<AssetLink>,
339    /// ID of the decision this supersedes
340    #[serde(skip_serializing_if = "Option::is_none")]
341    pub supersedes: Option<Uuid>,
342    /// ID of the decision that superseded this
343    #[serde(skip_serializing_if = "Option::is_none")]
344    pub superseded_by: Option<Uuid>,
345
346    // Compliance (from feature request)
347    /// Compliance assessment
348    #[serde(skip_serializing_if = "Option::is_none")]
349    pub compliance: Option<ComplianceAssessment>,
350
351    // Confirmation tracking (from feature request)
352    /// Date the decision was confirmed/reviewed
353    #[serde(skip_serializing_if = "Option::is_none")]
354    pub confirmation_date: Option<DateTime<Utc>>,
355    /// Notes from confirmation review
356    #[serde(skip_serializing_if = "Option::is_none")]
357    pub confirmation_notes: Option<String>,
358
359    // Standard metadata
360    /// Tags for categorization
361    #[serde(default, skip_serializing_if = "Vec::is_empty")]
362    pub tags: Vec<Tag>,
363    /// Additional notes
364    #[serde(skip_serializing_if = "Option::is_none")]
365    pub notes: Option<String>,
366
367    /// Creation timestamp
368    pub created_at: DateTime<Utc>,
369    /// Last modification timestamp
370    pub updated_at: DateTime<Utc>,
371}
372
373impl Decision {
374    /// Create a new decision with required fields
375    pub fn new(
376        number: u32,
377        title: impl Into<String>,
378        context: impl Into<String>,
379        decision: impl Into<String>,
380    ) -> Self {
381        let now = Utc::now();
382        Self {
383            id: Self::generate_id(number),
384            number,
385            title: title.into(),
386            status: DecisionStatus::Proposed,
387            category: DecisionCategory::Architecture,
388            domain: None,
389            date: now,
390            deciders: Vec::new(),
391            context: context.into(),
392            drivers: Vec::new(),
393            options: Vec::new(),
394            decision: decision.into(),
395            consequences: None,
396            linked_assets: Vec::new(),
397            supersedes: None,
398            superseded_by: None,
399            compliance: None,
400            confirmation_date: None,
401            confirmation_notes: None,
402            tags: Vec::new(),
403            notes: None,
404            created_at: now,
405            updated_at: now,
406        }
407    }
408
409    /// Generate a deterministic UUID for a decision based on its number
410    pub fn generate_id(number: u32) -> Uuid {
411        // Use UUID v5 with a namespace for decisions
412        let namespace = Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap(); // URL namespace
413        let name = format!("decision:{}", number);
414        Uuid::new_v5(&namespace, name.as_bytes())
415    }
416
417    /// Set the decision status
418    pub fn with_status(mut self, status: DecisionStatus) -> Self {
419        self.status = status;
420        self.updated_at = Utc::now();
421        self
422    }
423
424    /// Set the decision category
425    pub fn with_category(mut self, category: DecisionCategory) -> Self {
426        self.category = category;
427        self.updated_at = Utc::now();
428        self
429    }
430
431    /// Set the domain
432    pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
433        self.domain = Some(domain.into());
434        self.updated_at = Utc::now();
435        self
436    }
437
438    /// Add a decider
439    pub fn add_decider(mut self, decider: impl Into<String>) -> Self {
440        self.deciders.push(decider.into());
441        self.updated_at = Utc::now();
442        self
443    }
444
445    /// Add a driver
446    pub fn add_driver(mut self, driver: DecisionDriver) -> Self {
447        self.drivers.push(driver);
448        self.updated_at = Utc::now();
449        self
450    }
451
452    /// Add an option
453    pub fn add_option(mut self, option: DecisionOption) -> Self {
454        self.options.push(option);
455        self.updated_at = Utc::now();
456        self
457    }
458
459    /// Set consequences
460    pub fn with_consequences(mut self, consequences: impl Into<String>) -> Self {
461        self.consequences = Some(consequences.into());
462        self.updated_at = Utc::now();
463        self
464    }
465
466    /// Add an asset link
467    pub fn add_asset_link(mut self, link: AssetLink) -> Self {
468        self.linked_assets.push(link);
469        self.updated_at = Utc::now();
470        self
471    }
472
473    /// Set compliance assessment
474    pub fn with_compliance(mut self, compliance: ComplianceAssessment) -> Self {
475        self.compliance = Some(compliance);
476        self.updated_at = Utc::now();
477        self
478    }
479
480    /// Mark this decision as superseding another
481    pub fn supersedes_decision(mut self, other_id: Uuid) -> Self {
482        self.supersedes = Some(other_id);
483        self.updated_at = Utc::now();
484        self
485    }
486
487    /// Mark this decision as superseded by another
488    pub fn superseded_by_decision(&mut self, other_id: Uuid) {
489        self.superseded_by = Some(other_id);
490        self.status = DecisionStatus::Superseded;
491        self.updated_at = Utc::now();
492    }
493
494    /// Add a tag
495    pub fn add_tag(mut self, tag: Tag) -> Self {
496        self.tags.push(tag);
497        self.updated_at = Utc::now();
498        self
499    }
500
501    /// Generate the YAML filename for this decision
502    pub fn filename(&self, workspace_name: &str) -> String {
503        match &self.domain {
504            Some(domain) => format!(
505                "{}_{}_adr-{:04}.madr.yaml",
506                sanitize_name(workspace_name),
507                sanitize_name(domain),
508                self.number
509            ),
510            None => format!(
511                "{}_adr-{:04}.madr.yaml",
512                sanitize_name(workspace_name),
513                self.number
514            ),
515        }
516    }
517
518    /// Generate the Markdown filename for this decision
519    pub fn markdown_filename(&self) -> String {
520        let slug = slugify(&self.title);
521        format!("ADR-{:04}-{}.md", self.number, slug)
522    }
523
524    /// Import from YAML
525    pub fn from_yaml(yaml_content: &str) -> Result<Self, serde_yaml::Error> {
526        serde_yaml::from_str(yaml_content)
527    }
528
529    /// Export to YAML
530    pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
531        serde_yaml::to_string(self)
532    }
533
534    /// Export to pretty YAML
535    pub fn to_yaml_pretty(&self) -> Result<String, serde_yaml::Error> {
536        // serde_yaml already produces pretty output
537        serde_yaml::to_string(self)
538    }
539}
540
541/// Decision index entry for the decisions.yaml file
542#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
543pub struct DecisionIndexEntry {
544    /// Decision number
545    pub number: u32,
546    /// Decision UUID
547    pub id: Uuid,
548    /// Decision title
549    pub title: String,
550    /// Decision status
551    pub status: DecisionStatus,
552    /// Decision category
553    pub category: DecisionCategory,
554    /// Domain (if applicable)
555    #[serde(skip_serializing_if = "Option::is_none")]
556    pub domain: Option<String>,
557    /// Filename of the decision YAML file
558    pub file: String,
559}
560
561impl From<&Decision> for DecisionIndexEntry {
562    fn from(decision: &Decision) -> Self {
563        Self {
564            number: decision.number,
565            id: decision.id,
566            title: decision.title.clone(),
567            status: decision.status.clone(),
568            category: decision.category.clone(),
569            domain: decision.domain.clone(),
570            file: String::new(), // Set by caller
571        }
572    }
573}
574
575/// Decision log index (decisions.yaml)
576#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
577pub struct DecisionIndex {
578    /// Schema version
579    pub schema_version: String,
580    /// Last update timestamp
581    #[serde(skip_serializing_if = "Option::is_none")]
582    pub last_updated: Option<DateTime<Utc>>,
583    /// List of decisions
584    #[serde(default)]
585    pub decisions: Vec<DecisionIndexEntry>,
586    /// Next available decision number
587    pub next_number: u32,
588}
589
590impl Default for DecisionIndex {
591    fn default() -> Self {
592        Self::new()
593    }
594}
595
596impl DecisionIndex {
597    /// Create a new empty decision index
598    pub fn new() -> Self {
599        Self {
600            schema_version: "1.0".to_string(),
601            last_updated: Some(Utc::now()),
602            decisions: Vec::new(),
603            next_number: 1,
604        }
605    }
606
607    /// Add a decision to the index
608    pub fn add_decision(&mut self, decision: &Decision, filename: String) {
609        let mut entry = DecisionIndexEntry::from(decision);
610        entry.file = filename;
611
612        // Remove existing entry with same number if present
613        self.decisions.retain(|d| d.number != decision.number);
614        self.decisions.push(entry);
615
616        // Sort by number
617        self.decisions.sort_by_key(|d| d.number);
618
619        // Update next number
620        if decision.number >= self.next_number {
621            self.next_number = decision.number + 1;
622        }
623
624        self.last_updated = Some(Utc::now());
625    }
626
627    /// Get the next available decision number
628    pub fn get_next_number(&self) -> u32 {
629        self.next_number
630    }
631
632    /// Find a decision by number
633    pub fn find_by_number(&self, number: u32) -> Option<&DecisionIndexEntry> {
634        self.decisions.iter().find(|d| d.number == number)
635    }
636
637    /// Import from YAML
638    pub fn from_yaml(yaml_content: &str) -> Result<Self, serde_yaml::Error> {
639        serde_yaml::from_str(yaml_content)
640    }
641
642    /// Export to YAML
643    pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
644        serde_yaml::to_string(self)
645    }
646}
647
648/// Sanitize a name for use in filenames
649fn sanitize_name(name: &str) -> String {
650    name.chars()
651        .map(|c| match c {
652            ' ' | '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '-',
653            _ => c,
654        })
655        .collect::<String>()
656        .to_lowercase()
657}
658
659/// Create a URL-friendly slug from a title
660fn slugify(title: &str) -> String {
661    title
662        .to_lowercase()
663        .chars()
664        .map(|c| if c.is_alphanumeric() { c } else { '-' })
665        .collect::<String>()
666        .split('-')
667        .filter(|s| !s.is_empty())
668        .collect::<Vec<_>>()
669        .join("-")
670        .chars()
671        .take(50) // Limit slug length
672        .collect()
673}
674
675#[cfg(test)]
676mod tests {
677    use super::*;
678
679    #[test]
680    fn test_decision_new() {
681        let decision = Decision::new(
682            1,
683            "Use ODCS v3.1.0",
684            "We need a standard format",
685            "We will use ODCS v3.1.0",
686        );
687
688        assert_eq!(decision.number, 1);
689        assert_eq!(decision.title, "Use ODCS v3.1.0");
690        assert_eq!(decision.status, DecisionStatus::Proposed);
691        assert_eq!(decision.category, DecisionCategory::Architecture);
692    }
693
694    #[test]
695    fn test_decision_builder_pattern() {
696        let decision = Decision::new(1, "Test", "Context", "Decision")
697            .with_status(DecisionStatus::Accepted)
698            .with_category(DecisionCategory::DataDesign)
699            .with_domain("sales")
700            .add_decider("team@example.com")
701            .add_driver(DecisionDriver::with_priority(
702                "Need consistency",
703                DriverPriority::High,
704            ))
705            .with_consequences("Better consistency");
706
707        assert_eq!(decision.status, DecisionStatus::Accepted);
708        assert_eq!(decision.category, DecisionCategory::DataDesign);
709        assert_eq!(decision.domain, Some("sales".to_string()));
710        assert_eq!(decision.deciders.len(), 1);
711        assert_eq!(decision.drivers.len(), 1);
712        assert!(decision.consequences.is_some());
713    }
714
715    #[test]
716    fn test_decision_id_generation() {
717        let id1 = Decision::generate_id(1);
718        let id2 = Decision::generate_id(1);
719        let id3 = Decision::generate_id(2);
720
721        // Same number should generate same ID
722        assert_eq!(id1, id2);
723        // Different numbers should generate different IDs
724        assert_ne!(id1, id3);
725    }
726
727    #[test]
728    fn test_decision_filename() {
729        let decision = Decision::new(1, "Test", "Context", "Decision");
730        assert_eq!(
731            decision.filename("enterprise"),
732            "enterprise_adr-0001.madr.yaml"
733        );
734
735        let decision_with_domain = decision.with_domain("sales");
736        assert_eq!(
737            decision_with_domain.filename("enterprise"),
738            "enterprise_sales_adr-0001.madr.yaml"
739        );
740    }
741
742    #[test]
743    fn test_decision_markdown_filename() {
744        let decision = Decision::new(
745            1,
746            "Use ODCS v3.1.0 for all data contracts",
747            "Context",
748            "Decision",
749        );
750        let filename = decision.markdown_filename();
751        assert!(filename.starts_with("ADR-0001-"));
752        assert!(filename.ends_with(".md"));
753    }
754
755    #[test]
756    fn test_decision_yaml_roundtrip() {
757        let decision = Decision::new(1, "Test Decision", "Some context", "The decision")
758            .with_status(DecisionStatus::Accepted)
759            .with_domain("test");
760
761        let yaml = decision.to_yaml().unwrap();
762        let parsed = Decision::from_yaml(&yaml).unwrap();
763
764        assert_eq!(decision.id, parsed.id);
765        assert_eq!(decision.title, parsed.title);
766        assert_eq!(decision.status, parsed.status);
767        assert_eq!(decision.domain, parsed.domain);
768    }
769
770    #[test]
771    fn test_decision_index() {
772        let mut index = DecisionIndex::new();
773        assert_eq!(index.get_next_number(), 1);
774
775        let decision1 = Decision::new(1, "First", "Context", "Decision");
776        index.add_decision(&decision1, "test_adr-0001.madr.yaml".to_string());
777
778        assert_eq!(index.decisions.len(), 1);
779        assert_eq!(index.get_next_number(), 2);
780
781        let decision2 = Decision::new(2, "Second", "Context", "Decision");
782        index.add_decision(&decision2, "test_adr-0002.madr.yaml".to_string());
783
784        assert_eq!(index.decisions.len(), 2);
785        assert_eq!(index.get_next_number(), 3);
786    }
787
788    #[test]
789    fn test_slugify() {
790        assert_eq!(slugify("Use ODCS v3.1.0"), "use-odcs-v3-1-0");
791        assert_eq!(slugify("Hello World"), "hello-world");
792        assert_eq!(slugify("test--double"), "test-double");
793    }
794
795    #[test]
796    fn test_decision_status_display() {
797        assert_eq!(format!("{}", DecisionStatus::Proposed), "Proposed");
798        assert_eq!(format!("{}", DecisionStatus::Accepted), "Accepted");
799        assert_eq!(format!("{}", DecisionStatus::Deprecated), "Deprecated");
800        assert_eq!(format!("{}", DecisionStatus::Superseded), "Superseded");
801    }
802
803    #[test]
804    fn test_asset_link() {
805        let link = AssetLink::with_relationship(
806            "odcs",
807            Uuid::new_v4(),
808            "orders",
809            AssetRelationship::Implements,
810        );
811
812        assert_eq!(link.asset_type, "odcs");
813        assert_eq!(link.asset_name, "orders");
814        assert_eq!(link.relationship, Some(AssetRelationship::Implements));
815    }
816}