metis_core/domain/documents/
traits.rs

1use super::content::DocumentContent;
2use super::metadata::DocumentMetadata;
3use super::types::{DocumentId, DocumentType, Phase, Tag};
4use chrono::Utc;
5
6/// Core document trait that all document types must implement
7pub trait Document {
8    /// Get the unique identifier for this document (derived from title)
9    fn id(&self) -> DocumentId {
10        DocumentId::from_title(self.title())
11    }
12
13    /// Get the document type
14    fn document_type(&self) -> DocumentType;
15
16    /// Get the document title
17    fn title(&self) -> &str;
18
19    /// Get the document metadata
20    fn metadata(&self) -> &DocumentMetadata;
21
22    /// Get the document content
23    fn content(&self) -> &DocumentContent;
24
25    /// Get access to the core document data
26    fn core(&self) -> &DocumentCore;
27
28    /// Get the document tags
29    fn tags(&self) -> &[Tag] {
30        &self.core().tags
31    }
32
33    /// Get the current phase of the document (parsed from tags)
34    fn phase(&self) -> Result<Phase, DocumentValidationError> {
35        // Find the first Phase tag in the tags list
36        for tag in self.tags() {
37            if let Tag::Phase(phase) = tag {
38                return Ok(*phase);
39            }
40        }
41        // No phase tag found - this is an error
42        Err(DocumentValidationError::MissingPhaseTag)
43    }
44
45    /// Check if this document can transition to the given phase
46    fn can_transition_to(&self, phase: Phase) -> bool;
47
48    /// Transition to the next phase in sequence, or to a specific phase if provided
49    fn transition_phase(
50        &mut self,
51        target_phase: Option<Phase>,
52    ) -> Result<Phase, DocumentValidationError>;
53
54    /// Update a specific section (H2 heading) in the document content
55    fn update_section(
56        &mut self,
57        content: &str,
58        heading: &str,
59        append: bool,
60    ) -> Result<(), DocumentValidationError> {
61        let lines: Vec<&str> = self.core().content.body.lines().collect();
62        let target_heading = format!("## {}", heading);
63
64        // Find the section start
65        let section_start = lines.iter().position(|line| line.trim() == target_heading);
66
67        let new_body = if let Some(section_start) = section_start {
68            // Section exists, update it
69            let section_end = lines[section_start + 1..]
70                .iter()
71                .position(|line| line.trim_start().starts_with("## "))
72                .map(|pos| section_start + 1 + pos)
73                .unwrap_or(lines.len());
74
75            // Build the updated content
76            let mut updated_lines = Vec::new();
77
78            // Add content before the section
79            updated_lines.extend_from_slice(&lines[..section_start + 1]);
80
81            if append {
82                // For append mode, keep existing content and add new content
83                updated_lines.extend_from_slice(&lines[section_start + 1..section_end]);
84                if !content.trim().is_empty() {
85                    if section_end > section_start + 1 {
86                        updated_lines.push(""); // Add blank line before new content
87                    }
88                    for line in content.lines() {
89                        updated_lines.push(line);
90                    }
91                }
92            } else {
93                // For replace mode, replace section content entirely
94                if !content.trim().is_empty() {
95                    updated_lines.push(""); // Empty line after heading
96                    for line in content.lines() {
97                        updated_lines.push(line);
98                    }
99                }
100            }
101
102            // Add content after the section
103            if section_end < lines.len() {
104                updated_lines.push(""); // Empty line before next section
105                updated_lines.extend_from_slice(&lines[section_end..]);
106            }
107
108            updated_lines.join("\n")
109        } else {
110            // Section doesn't exist, add new section
111            let mut updated_lines: Vec<String> = lines.iter().map(|s| s.to_string()).collect();
112
113            if !updated_lines.is_empty() {
114                updated_lines.push("".to_string()); // Empty line before new section
115            }
116            updated_lines.push(target_heading);
117            if !content.trim().is_empty() {
118                updated_lines.push("".to_string()); // Empty line after heading
119                for line in content.lines() {
120                    updated_lines.push(line.to_string());
121                }
122            }
123
124            updated_lines.join("\n")
125        };
126
127        self.update_content_body(new_body)
128    }
129
130    /// Helper method for update_section to actually mutate the content
131    fn update_content_body(&mut self, new_body: String) -> Result<(), DocumentValidationError> {
132        // We need mutable access to core, which requires each document type to provide access
133        let core = self.core_mut();
134        core.content.body = new_body;
135        core.metadata.updated_at = Utc::now();
136        Ok(())
137    }
138
139    /// Get mutable access to the document core (needed for updates)
140    fn core_mut(&mut self) -> &mut DocumentCore;
141
142    /// Check if this document is archived
143    fn archived(&self) -> bool {
144        self.core().archived
145    }
146
147    /// Get the parent document ID if this document has a parent
148    fn parent_id(&self) -> Option<&DocumentId>;
149
150    /// Get IDs of documents that block this one
151    fn blocked_by(&self) -> &[DocumentId];
152
153    /// Validate the document according to its type-specific rules
154    fn validate(&self) -> Result<(), DocumentValidationError>;
155
156    /// Check if exit criteria are met
157    fn exit_criteria_met(&self) -> bool;
158
159    /// Get the template for rendering this document type
160    fn template(&self) -> DocumentTemplate;
161
162    /// Get the frontmatter template for this document type
163    fn frontmatter_template(&self) -> &'static str;
164
165    /// Get the content template for this document type
166    fn content_template(&self) -> &'static str;
167
168    /// Get the acceptance criteria template for this document type
169    fn acceptance_criteria_template(&self) -> &'static str;
170}
171
172/// Template information for a document
173pub struct DocumentTemplate {
174    pub frontmatter: &'static str,
175    pub content: &'static str,
176    pub acceptance_criteria: &'static str,
177    pub file_extension: &'static str,
178}
179
180/// Common document data that all document types share
181#[derive(Debug)]
182pub struct DocumentCore {
183    pub title: String,
184    pub metadata: DocumentMetadata,
185    pub content: DocumentContent,
186    pub parent_id: Option<DocumentId>,
187    pub blocked_by: Vec<DocumentId>,
188    pub tags: Vec<Tag>,
189    pub archived: bool,
190}
191
192/// Validation errors for documents
193#[derive(Debug, PartialEq, thiserror::Error)]
194pub enum DocumentValidationError {
195    #[error("Invalid title: {0}")]
196    InvalidTitle(String),
197
198    #[error("Invalid parent: {0}")]
199    InvalidParent(String),
200
201    #[error("Invalid phase transition from {from:?} to {to:?}")]
202    InvalidPhaseTransition { from: Phase, to: Phase },
203
204    #[error("Missing required field: {0}")]
205    MissingRequiredField(String),
206
207    #[error("Invalid content: {0}")]
208    InvalidContent(String),
209
210    #[error("Missing phase tag in document")]
211    MissingPhaseTag,
212}