metis_core/domain/documents/vision/
mod.rs

1use super::content::DocumentContent;
2use super::helpers::FrontmatterParser;
3use super::metadata::DocumentMetadata;
4use super::traits::{Document, DocumentTemplate, DocumentValidationError};
5use super::types::{DocumentId, DocumentType, Phase, Tag};
6use gray_matter;
7use std::path::Path;
8use tera::{Context, Tera};
9
10/// A Vision document represents the high-level direction and goals
11#[derive(Debug)]
12pub struct Vision {
13    core: super::traits::DocumentCore,
14}
15
16impl Vision {
17    /// Create a new Vision document with content rendered from template
18    pub fn new(
19        title: String,
20        tags: Vec<Tag>,
21        archived: bool,
22    ) -> Result<Self, DocumentValidationError> {
23        // Create fresh metadata
24        let metadata = DocumentMetadata::new();
25
26        // Render the content template
27        let template_content = include_str!("content.md");
28        let mut tera = Tera::default();
29        tera.add_raw_template("vision_content", template_content)
30            .map_err(|e| {
31                DocumentValidationError::InvalidContent(format!("Template error: {}", e))
32            })?;
33
34        let mut context = Context::new();
35        context.insert("title", &title);
36
37        let rendered_content = tera.render("vision_content", &context).map_err(|e| {
38            DocumentValidationError::InvalidContent(format!("Template render error: {}", e))
39        })?;
40
41        let content = DocumentContent::new(&rendered_content);
42
43        Ok(Self {
44            core: super::traits::DocumentCore {
45                title,
46                metadata,
47                content,
48                parent_id: None,        // Visions have no parents
49                blocked_by: Vec::new(), // Visions cannot be blocked
50                tags,
51                archived,
52            },
53        })
54    }
55
56    /// Create a Vision document from existing data (used when loading from file)
57    pub fn from_parts(
58        title: String,
59        metadata: DocumentMetadata,
60        content: DocumentContent,
61        tags: Vec<Tag>,
62        archived: bool,
63    ) -> Self {
64        Self {
65            core: super::traits::DocumentCore {
66                title,
67                metadata,
68                content,
69                parent_id: None,        // Visions have no parents
70                blocked_by: Vec::new(), // Visions cannot be blocked
71                tags,
72                archived,
73            },
74        }
75    }
76
77    /// Create a Vision document by reading and parsing a file
78    pub async fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, DocumentValidationError> {
79        let raw_content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
80            DocumentValidationError::InvalidContent(format!("Failed to read file: {}", e))
81        })?;
82
83        Self::from_content(&raw_content)
84    }
85
86    /// Create a Vision document from raw file content string
87    pub fn from_content(raw_content: &str) -> Result<Self, DocumentValidationError> {
88        // Parse frontmatter and content
89        let parsed = gray_matter::Matter::<gray_matter::engine::YAML>::new().parse(raw_content);
90
91        // Extract frontmatter data
92        let frontmatter = parsed.data.ok_or_else(|| {
93            DocumentValidationError::MissingRequiredField("frontmatter".to_string())
94        })?;
95
96        // Parse frontmatter into structured data
97        let fm_map = match frontmatter {
98            gray_matter::Pod::Hash(map) => map,
99            _ => {
100                return Err(DocumentValidationError::InvalidContent(
101                    "Frontmatter must be a hash/map".to_string(),
102                ))
103            }
104        };
105
106        // Extract required fields
107        let title = FrontmatterParser::extract_string(&fm_map, "title")?;
108        let archived = FrontmatterParser::extract_bool(&fm_map, "archived").unwrap_or(false);
109
110        // Parse timestamps
111        let created_at = FrontmatterParser::extract_datetime(&fm_map, "created_at")?;
112        let updated_at = FrontmatterParser::extract_datetime(&fm_map, "updated_at")?;
113        let exit_criteria_met =
114            FrontmatterParser::extract_bool(&fm_map, "exit_criteria_met").unwrap_or(false);
115
116        // Parse tags
117        let tags = FrontmatterParser::extract_tags(&fm_map)?;
118
119        // Verify this is actually a vision document
120        let level = FrontmatterParser::extract_string(&fm_map, "level")?;
121        if level != "vision" {
122            return Err(DocumentValidationError::InvalidContent(format!(
123                "Expected level 'vision', found '{}'",
124                level
125            )));
126        }
127
128        // Create metadata and content
129        let metadata =
130            DocumentMetadata::from_frontmatter(created_at, updated_at, exit_criteria_met);
131        let content = DocumentContent::from_markdown(&parsed.content);
132
133        Ok(Self::from_parts(title, metadata, content, tags, archived))
134    }
135
136    /// Get the next phase in the Vision sequence
137    fn next_phase_in_sequence(current: Phase) -> Option<Phase> {
138        use Phase::*;
139        match current {
140            Draft => Some(Review),
141            Review => Some(Published),
142            Published => None, // Final phase
143            _ => None,         // Invalid phase for Vision
144        }
145    }
146
147    /// Update the phase tag in the document's tags
148    fn update_phase_tag(&mut self, new_phase: Phase) {
149        // Remove any existing phase tags
150        self.core.tags.retain(|tag| !matches!(tag, Tag::Phase(_)));
151        // Add the new phase tag
152        self.core.tags.push(Tag::Phase(new_phase));
153        // Update timestamp
154        self.core.metadata.updated_at = chrono::Utc::now();
155    }
156
157    /// Write the Vision document to a file
158    pub async fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), DocumentValidationError> {
159        let content = self.to_content()?;
160        std::fs::write(path.as_ref(), content).map_err(|e| {
161            DocumentValidationError::InvalidContent(format!("Failed to write file: {}", e))
162        })
163    }
164
165    /// Convert the Vision document to its markdown string representation using templates
166    pub fn to_content(&self) -> Result<String, DocumentValidationError> {
167        let mut tera = Tera::default();
168
169        // Add the frontmatter template to Tera
170        tera.add_raw_template("frontmatter", self.frontmatter_template())
171            .map_err(|e| {
172                DocumentValidationError::InvalidContent(format!("Template error: {}", e))
173            })?;
174
175        // Create context with all document data
176        let mut context = Context::new();
177        context.insert("slug", &self.id().to_string());
178        context.insert("title", self.title());
179        context.insert("created_at", &self.metadata().created_at.to_rfc3339());
180        context.insert("updated_at", &self.metadata().updated_at.to_rfc3339());
181        context.insert("archived", &self.archived().to_string());
182        context.insert(
183            "exit_criteria_met",
184            &self.metadata().exit_criteria_met.to_string(),
185        );
186
187        // Convert tags to strings
188        let tag_strings: Vec<String> = self.tags().iter().map(|tag| tag.to_str()).collect();
189        context.insert("tags", &tag_strings);
190
191        // Render frontmatter
192        let frontmatter = tera.render("frontmatter", &context).map_err(|e| {
193            DocumentValidationError::InvalidContent(format!("Frontmatter render error: {}", e))
194        })?;
195
196        // Use the actual content body
197        let content_body = &self.content().body;
198
199        // Use actual acceptance criteria if present, otherwise empty string
200        let acceptance_criteria = if let Some(ac) = &self.content().acceptance_criteria {
201            format!("\n\n## Acceptance Criteria\n\n{}", ac)
202        } else {
203            String::new()
204        };
205
206        // Combine everything
207        Ok(format!(
208            "---\n{}\n---\n\n{}{}",
209            frontmatter.trim_end(),
210            content_body,
211            acceptance_criteria
212        ))
213    }
214}
215
216impl Document for Vision {
217    // id() is provided by the trait with default implementation
218
219    fn document_type(&self) -> DocumentType {
220        DocumentType::Vision
221    }
222
223    fn title(&self) -> &str {
224        &self.core.title
225    }
226
227    fn metadata(&self) -> &DocumentMetadata {
228        &self.core.metadata
229    }
230
231    fn content(&self) -> &DocumentContent {
232        &self.core.content
233    }
234
235    fn core(&self) -> &super::traits::DocumentCore {
236        &self.core
237    }
238
239    fn can_transition_to(&self, phase: Phase) -> bool {
240        if let Ok(current_phase) = self.phase() {
241            use Phase::*;
242            matches!((current_phase, phase), (Draft, Review) | (Review, Published))
243        } else {
244            false // Can't transition if we can't determine current phase
245        }
246    }
247
248    fn parent_id(&self) -> Option<&DocumentId> {
249        None // Visions never have parents
250    }
251
252    fn blocked_by(&self) -> &[DocumentId] {
253        &[] // Visions can never be blocked
254    }
255
256    fn validate(&self) -> Result<(), DocumentValidationError> {
257        // Vision-specific validation rules
258        if self.title().trim().is_empty() {
259            return Err(DocumentValidationError::InvalidTitle(
260                "Vision title cannot be empty".to_string(),
261            ));
262        }
263
264        if self.parent_id().is_some() {
265            return Err(DocumentValidationError::InvalidParent(
266                "Visions cannot have parents".to_string(),
267            ));
268        }
269
270        if !self.blocked_by().is_empty() {
271            return Err(DocumentValidationError::InvalidContent(
272                "Visions cannot be blocked by other documents".to_string(),
273            ));
274        }
275
276        Ok(())
277    }
278
279    fn exit_criteria_met(&self) -> bool {
280        // Check if all acceptance criteria checkboxes are checked
281        // This would typically parse the content for checkbox completion
282        // For now, return false as a placeholder
283        false
284    }
285
286    fn template(&self) -> DocumentTemplate {
287        DocumentTemplate {
288            frontmatter: self.frontmatter_template(),
289            content: self.content_template(),
290            acceptance_criteria: self.acceptance_criteria_template(),
291            file_extension: "md",
292        }
293    }
294
295    fn frontmatter_template(&self) -> &'static str {
296        include_str!("frontmatter.yaml")
297    }
298
299    fn content_template(&self) -> &'static str {
300        include_str!("content.md")
301    }
302
303    fn acceptance_criteria_template(&self) -> &'static str {
304        include_str!("acceptance_criteria.md")
305    }
306
307    fn transition_phase(
308        &mut self,
309        target_phase: Option<Phase>,
310    ) -> Result<Phase, DocumentValidationError> {
311        let current_phase = self.phase()?;
312
313        let new_phase = match target_phase {
314            Some(phase) => {
315                // Validate the transition is allowed
316                if !self.can_transition_to(phase) {
317                    return Err(DocumentValidationError::InvalidPhaseTransition {
318                        from: current_phase,
319                        to: phase,
320                    });
321                }
322                phase
323            }
324            None => {
325                // Auto-transition to next phase in sequence
326                match Self::next_phase_in_sequence(current_phase) {
327                    Some(next) => next,
328                    None => return Ok(current_phase), // Already at final phase
329                }
330            }
331        };
332
333        self.update_phase_tag(new_phase);
334        Ok(new_phase)
335    }
336
337    fn core_mut(&mut self) -> &mut super::traits::DocumentCore {
338        &mut self.core
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345    use crate::domain::documents::traits::DocumentValidationError;
346    use tempfile::tempdir;
347
348    #[tokio::test]
349    async fn test_vision_from_content() {
350        let content = r##"---
351id: test-vision
352level: vision
353title: "Test Vision"
354created_at: 2025-01-01T00:00:00Z
355updated_at: 2025-01-01T00:00:00Z
356archived: false
357
358tags:
359  - "#vision"
360  - "#phase/draft"
361
362exit_criteria_met: false
363---
364
365# Test Vision
366
367## Purpose
368
369This is a test vision for our system.
370
371## Current State
372
373We are here.
374
375## Future State
376
377We want to be there.
378
379## Acceptance Criteria
380
381- [ ] Purpose is clearly defined
382- [ ] Current and future states are documented
383"##;
384
385        let vision = Vision::from_content(content).unwrap();
386
387        assert_eq!(vision.title(), "Test Vision");
388        assert_eq!(vision.document_type(), DocumentType::Vision);
389        assert!(!vision.archived());
390        assert_eq!(vision.tags().len(), 2);
391        assert_eq!(vision.phase().unwrap(), Phase::Draft);
392        assert!(vision.content().has_acceptance_criteria());
393
394        // Round-trip test: write to file and read back
395        let temp_dir = tempdir().unwrap();
396        let file_path = temp_dir.path().join("test-vision.md");
397
398        vision.to_file(&file_path).await.unwrap();
399        let loaded_vision = Vision::from_file(&file_path).await.unwrap();
400
401        assert_eq!(loaded_vision.title(), vision.title());
402        assert_eq!(loaded_vision.phase().unwrap(), vision.phase().unwrap());
403        assert_eq!(loaded_vision.content().body, vision.content().body);
404        assert_eq!(loaded_vision.archived(), vision.archived());
405        assert_eq!(loaded_vision.tags().len(), vision.tags().len());
406    }
407
408    #[test]
409    fn test_vision_invalid_level() {
410        let content = r##"---
411id: test-doc
412level: strategy
413title: "Test Strategy"
414created_at: 2025-01-01T00:00:00Z
415updated_at: 2025-01-01T00:00:00Z
416archived: false
417tags:
418  - "#strategy"
419  - "#phase/shaping"
420exit_criteria_met: false
421---
422
423# Test Strategy
424"##;
425
426        let result = Vision::from_content(content);
427        assert!(result.is_err());
428        match result.unwrap_err() {
429            DocumentValidationError::InvalidContent(msg) => {
430                assert!(msg.contains("Expected level 'vision'"));
431            }
432            _ => panic!("Expected InvalidContent error"),
433        }
434    }
435
436    #[test]
437    fn test_vision_missing_title() {
438        let content = r##"---
439id: test-vision
440level: vision
441created_at: 2025-01-01T00:00:00Z
442updated_at: 2025-01-01T00:00:00Z
443archived: false
444tags:
445  - "#vision"
446  - "#phase/draft"
447exit_criteria_met: false
448---
449
450Some content without a title in frontmatter.
451"##;
452
453        let result = Vision::from_content(content);
454        assert!(result.is_err());
455        match result.unwrap_err() {
456            DocumentValidationError::MissingRequiredField(field) => {
457                assert_eq!(field, "title");
458            }
459            _ => panic!("Expected MissingRequiredField error"),
460        }
461    }
462
463    #[tokio::test]
464    async fn test_vision_tag_parsing() {
465        let content = r##"---
466id: test-vision
467level: vision
468title: "Test Vision"
469created_at: 2025-01-01T00:00:00Z
470updated_at: 2025-01-01T00:00:00Z
471archived: false
472tags:
473  - "#vision"
474  - "#phase/review"
475  - "#high-priority"
476  - "urgent"
477exit_criteria_met: false
478---
479
480# Test Vision
481"##;
482
483        let vision = Vision::from_content(content).unwrap();
484        let tags = vision.tags();
485
486        assert_eq!(tags.len(), 4);
487        assert!(tags.contains(&Tag::Label("vision".to_string())));
488        assert!(tags.contains(&Tag::Phase(Phase::Review)));
489        assert!(tags.contains(&Tag::Label("high-priority".to_string())));
490        assert!(tags.contains(&Tag::Label("urgent".to_string())));
491
492        assert_eq!(vision.phase().unwrap(), Phase::Review);
493
494        // Round-trip test: write to file and read back
495        let temp_dir = tempdir().unwrap();
496        let file_path = temp_dir.path().join("test-vision.md");
497
498        vision.to_file(&file_path).await.unwrap();
499        let loaded_vision = Vision::from_file(&file_path).await.unwrap();
500
501        assert_eq!(loaded_vision.title(), vision.title());
502        assert_eq!(loaded_vision.phase().unwrap(), vision.phase().unwrap());
503        assert_eq!(loaded_vision.tags().len(), vision.tags().len());
504
505        // Verify specific tags survived the round-trip
506        let loaded_tags = loaded_vision.tags();
507        assert!(loaded_tags.contains(&Tag::Label("vision".to_string())));
508        assert!(loaded_tags.contains(&Tag::Phase(Phase::Review)));
509        assert!(loaded_tags.contains(&Tag::Label("high-priority".to_string())));
510        assert!(loaded_tags.contains(&Tag::Label("urgent".to_string())));
511    }
512
513    #[tokio::test]
514    async fn test_vision_validation() {
515        let vision = Vision::new(
516            "Test Vision".to_string(),
517            vec![Tag::Label("vision".to_string()), Tag::Phase(Phase::Draft)],
518            false,
519        )
520        .expect("Failed to create vision");
521
522        assert!(vision.validate().is_ok());
523        assert_eq!(vision.parent_id(), None);
524        assert_eq!(vision.blocked_by().len(), 0);
525
526        // Round-trip test: write to file and read back
527        let temp_dir = tempdir().unwrap();
528        let file_path = temp_dir.path().join("test-vision.md");
529
530        // Write to file
531        vision.to_file(&file_path).await.unwrap();
532
533        // Read back from file
534        let loaded_vision = Vision::from_file(&file_path).await.unwrap();
535
536        // Verify all fields match
537        assert_eq!(loaded_vision.title(), vision.title());
538        assert_eq!(loaded_vision.phase().unwrap(), vision.phase().unwrap());
539        assert_eq!(loaded_vision.content().body, vision.content().body);
540        assert_eq!(loaded_vision.archived(), vision.archived());
541        assert_eq!(loaded_vision.tags().len(), vision.tags().len());
542    }
543
544    #[tokio::test]
545    async fn test_vision_phase_transitions() {
546        let vision = Vision::new(
547            "Test Vision".to_string(),
548            vec![Tag::Phase(Phase::Draft)],
549            false,
550        )
551        .expect("Failed to create vision");
552
553        assert!(vision.can_transition_to(Phase::Review));
554        assert!(!vision.can_transition_to(Phase::Published));
555        assert!(!vision.can_transition_to(Phase::Active));
556
557        // Round-trip test: write to file and read back
558        let temp_dir = tempdir().unwrap();
559        let file_path = temp_dir.path().join("test-vision.md");
560
561        vision.to_file(&file_path).await.unwrap();
562        let loaded_vision = Vision::from_file(&file_path).await.unwrap();
563
564        assert_eq!(loaded_vision.title(), vision.title());
565        assert_eq!(loaded_vision.phase().unwrap(), vision.phase().unwrap());
566        assert!(loaded_vision.can_transition_to(Phase::Review));
567        assert!(!loaded_vision.can_transition_to(Phase::Published));
568        assert!(!loaded_vision.can_transition_to(Phase::Active));
569    }
570
571    #[tokio::test]
572    async fn test_vision_transition_phase_auto() {
573        let mut vision = Vision::new(
574            "Test Vision".to_string(),
575            vec![Tag::Phase(Phase::Draft)],
576            false,
577        )
578        .expect("Failed to create vision");
579
580        // Auto-transition from Draft should go to Review
581        let new_phase = vision.transition_phase(None).unwrap();
582        assert_eq!(new_phase, Phase::Review);
583        assert_eq!(vision.phase().unwrap(), Phase::Review);
584
585        // Round-trip test after first transition
586        let temp_dir = tempdir().unwrap();
587        let file_path = temp_dir.path().join("test-vision.md");
588        vision.to_file(&file_path).await.unwrap();
589        let mut loaded_vision = Vision::from_file(&file_path).await.unwrap();
590        assert_eq!(loaded_vision.phase().unwrap(), Phase::Review);
591
592        // Auto-transition from Review should go to Published
593        let new_phase = loaded_vision.transition_phase(None).unwrap();
594        assert_eq!(new_phase, Phase::Published);
595        assert_eq!(loaded_vision.phase().unwrap(), Phase::Published);
596
597        // Round-trip test after second transition
598        loaded_vision.to_file(&file_path).await.unwrap();
599        let mut loaded_vision2 = Vision::from_file(&file_path).await.unwrap();
600        assert_eq!(loaded_vision2.phase().unwrap(), Phase::Published);
601
602        // Auto-transition from Published should stay at Published (final phase)
603        let new_phase = loaded_vision2.transition_phase(None).unwrap();
604        assert_eq!(new_phase, Phase::Published);
605        assert_eq!(loaded_vision2.phase().unwrap(), Phase::Published);
606
607        // Final round-trip test
608        loaded_vision2.to_file(&file_path).await.unwrap();
609        let loaded_vision3 = Vision::from_file(&file_path).await.unwrap();
610        assert_eq!(loaded_vision3.phase().unwrap(), Phase::Published);
611    }
612
613    #[tokio::test]
614    async fn test_vision_transition_phase_explicit() {
615        let mut vision = Vision::new(
616            "Test Vision".to_string(),
617            vec![Tag::Phase(Phase::Draft)],
618            false,
619        )
620        .expect("Failed to create vision");
621
622        // Explicit transition from Draft to Review
623        let new_phase = vision.transition_phase(Some(Phase::Review)).unwrap();
624        assert_eq!(new_phase, Phase::Review);
625        assert_eq!(vision.phase().unwrap(), Phase::Review);
626
627        // Round-trip test after first transition
628        let temp_dir = tempdir().unwrap();
629        let file_path = temp_dir.path().join("test-vision.md");
630        vision.to_file(&file_path).await.unwrap();
631        let mut loaded_vision = Vision::from_file(&file_path).await.unwrap();
632        assert_eq!(loaded_vision.phase().unwrap(), Phase::Review);
633
634        // Explicit transition from Review to Published
635        let new_phase = loaded_vision
636            .transition_phase(Some(Phase::Published))
637            .unwrap();
638        assert_eq!(new_phase, Phase::Published);
639        assert_eq!(loaded_vision.phase().unwrap(), Phase::Published);
640
641        // Final round-trip test
642        loaded_vision.to_file(&file_path).await.unwrap();
643        let loaded_vision2 = Vision::from_file(&file_path).await.unwrap();
644        assert_eq!(loaded_vision2.phase().unwrap(), Phase::Published);
645    }
646
647    #[tokio::test]
648    async fn test_vision_transition_phase_invalid() {
649        let mut vision = Vision::new(
650            "Test Vision".to_string(),
651            vec![Tag::Phase(Phase::Draft)],
652            false,
653        )
654        .expect("Failed to create vision");
655
656        // Invalid transition from Draft to Published (must go through Review)
657        let result = vision.transition_phase(Some(Phase::Published));
658        assert!(result.is_err());
659        match result.unwrap_err() {
660            DocumentValidationError::InvalidPhaseTransition { from, to } => {
661                assert_eq!(from, Phase::Draft);
662                assert_eq!(to, Phase::Published);
663            }
664            _ => panic!("Expected InvalidPhaseTransition error"),
665        }
666
667        // Should still be in Draft phase
668        assert_eq!(vision.phase().unwrap(), Phase::Draft);
669
670        // Round-trip test to ensure vision is still valid after failed transition
671        let temp_dir = tempdir().unwrap();
672        let file_path = temp_dir.path().join("test-vision.md");
673        vision.to_file(&file_path).await.unwrap();
674        let loaded_vision = Vision::from_file(&file_path).await.unwrap();
675        assert_eq!(loaded_vision.phase().unwrap(), Phase::Draft);
676    }
677
678    #[tokio::test]
679    async fn test_vision_update_section() {
680        // First create a vision with the template
681        let mut vision = Vision::new(
682            "Test Vision".to_string(),
683            vec![Tag::Phase(Phase::Draft)],
684            false,
685        )
686        .expect("Failed to create vision");
687
688        // Then update its content to have specific test content
689        vision.core_mut().content = DocumentContent::new(
690            "## Purpose\n\nOriginal purpose\n\n## Current State\n\nOriginal state",
691        );
692
693        // Replace existing section
694        vision
695            .update_section("Updated purpose content", "Purpose", false)
696            .unwrap();
697        let content = vision.content().body.clone();
698        assert!(content.contains("## Purpose\n\nUpdated purpose content"));
699        assert!(!content.contains("Original purpose"));
700
701        // Append to existing section
702        vision
703            .update_section("Additional state info", "Current State", true)
704            .unwrap();
705        let content = vision.content().body.clone();
706        assert!(content.contains("Original state"));
707        assert!(content.contains("Additional state info"));
708
709        // Add new section
710        vision
711            .update_section("Future vision details", "Future State", false)
712            .unwrap();
713        let content = vision.content().body.clone();
714        assert!(content.contains("## Future State\n\nFuture vision details"));
715
716        // Round-trip test to ensure all updates persist
717        let temp_dir = tempdir().unwrap();
718        let file_path = temp_dir.path().join("test-vision.md");
719        vision.to_file(&file_path).await.unwrap();
720        let loaded_vision = Vision::from_file(&file_path).await.unwrap();
721
722        let loaded_content = loaded_vision.content().body.clone();
723        assert!(loaded_content.contains("## Purpose\n\nUpdated purpose content"));
724        assert!(loaded_content.contains("Original state"));
725        assert!(loaded_content.contains("Additional state info"));
726        assert!(loaded_content.contains("## Future State\n\nFuture vision details"));
727    }
728}