Skip to main content

data_modelling_core/models/
sketch.rs

1//! Sketch model for Excalidraw diagrams
2//!
3//! Implements sketch support for storing and organizing Excalidraw diagrams
4//! within workspaces. Sketches can be linked to knowledge articles, decisions,
5//! and other assets.
6//!
7//! ## File Format
8//!
9//! Sketches are stored as `.sketch.yaml` files following the naming convention:
10//! `{workspace}_{domain}_sketch-{number}.sketch.yaml`
11//!
12//! ## Example
13//!
14//! ```yaml
15//! id: 770e8400-e29b-41d4-a716-446655440001
16//! number: 1
17//! title: "Sales Domain Architecture"
18//! sketchType: architecture
19//! status: published
20//! domain: sales
21//! description: "High-level architecture diagram for sales domain"
22//! excalidrawData: '{"type":"excalidraw","version":2,"elements":[...]}'
23//! thumbnailPath: thumbnails/sketch-0001.png
24//! authors:
25//!   - architect@company.com
26//! ```
27
28use chrono::{DateTime, Utc};
29use serde::{Deserialize, Serialize};
30use uuid::Uuid;
31
32use super::Tag;
33use super::decision::AssetLink;
34
35/// Sketch status
36#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
37#[serde(rename_all = "lowercase")]
38pub enum SketchStatus {
39    /// Sketch is being drafted
40    #[default]
41    Draft,
42    /// Sketch is under review
43    Review,
44    /// Sketch is published and active
45    Published,
46    /// Sketch is archived (historical reference)
47    Archived,
48}
49
50impl std::fmt::Display for SketchStatus {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        match self {
53            SketchStatus::Draft => write!(f, "Draft"),
54            SketchStatus::Review => write!(f, "Review"),
55            SketchStatus::Published => write!(f, "Published"),
56            SketchStatus::Archived => write!(f, "Archived"),
57        }
58    }
59}
60
61/// Sketch type/category
62#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
63#[serde(rename_all = "lowercase")]
64pub enum SketchType {
65    /// Architecture diagram
66    #[default]
67    Architecture,
68    /// Data flow diagram
69    DataFlow,
70    /// Entity relationship diagram
71    EntityRelationship,
72    /// Sequence diagram
73    Sequence,
74    /// Flowchart
75    Flowchart,
76    /// Wireframe/mockup
77    Wireframe,
78    /// Concept/mind map
79    Concept,
80    /// Infrastructure diagram
81    Infrastructure,
82    /// Other/general sketch
83    Other,
84}
85
86impl std::fmt::Display for SketchType {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        match self {
89            SketchType::Architecture => write!(f, "Architecture"),
90            SketchType::DataFlow => write!(f, "Data Flow"),
91            SketchType::EntityRelationship => write!(f, "Entity Relationship"),
92            SketchType::Sequence => write!(f, "Sequence"),
93            SketchType::Flowchart => write!(f, "Flowchart"),
94            SketchType::Wireframe => write!(f, "Wireframe"),
95            SketchType::Concept => write!(f, "Concept"),
96            SketchType::Infrastructure => write!(f, "Infrastructure"),
97            SketchType::Other => write!(f, "Other"),
98        }
99    }
100}
101
102/// Custom deserializer for sketch number that supports both:
103/// - Legacy string format: "SKETCH-0001"
104/// - New numeric format: 1 or 2601101234 (timestamp)
105fn deserialize_sketch_number<'de, D>(deserializer: D) -> Result<u64, D::Error>
106where
107    D: serde::Deserializer<'de>,
108{
109    use serde::de::{self, Visitor};
110
111    struct NumberVisitor;
112
113    impl<'de> Visitor<'de> for NumberVisitor {
114        type Value = u64;
115
116        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
117            formatter.write_str("a number or a string like 'SKETCH-0001'")
118        }
119
120        fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
121        where
122            E: de::Error,
123        {
124            Ok(value)
125        }
126
127        fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
128        where
129            E: de::Error,
130        {
131            if value >= 0 {
132                Ok(value as u64)
133            } else {
134                Err(E::custom("negative numbers are not allowed"))
135            }
136        }
137
138        fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
139        where
140            E: de::Error,
141        {
142            // Handle "SKETCH-0001" format
143            let num_str = value
144                .to_uppercase()
145                .strip_prefix("SKETCH-")
146                .map(|s| s.to_string())
147                .unwrap_or_else(|| value.to_string());
148
149            num_str
150                .parse::<u64>()
151                .map_err(|_| E::custom(format!("invalid sketch number format: {}", value)))
152        }
153    }
154
155    deserializer.deserialize_any(NumberVisitor)
156}
157
158/// Excalidraw Sketch
159///
160/// Represents an Excalidraw sketch that can be categorized by domain,
161/// type, and linked to other assets.
162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
163#[serde(rename_all = "camelCase")]
164pub struct Sketch {
165    /// Unique identifier for the sketch
166    pub id: Uuid,
167    /// Sketch number - can be sequential (1, 2, 3) or timestamp-based (YYMMDDHHmm format)
168    /// Timestamp format prevents merge conflicts in distributed Git workflows
169    #[serde(deserialize_with = "deserialize_sketch_number")]
170    pub number: u64,
171    /// Sketch title
172    pub title: String,
173    /// Type of sketch
174    #[serde(alias = "sketch_type")]
175    pub sketch_type: SketchType,
176    /// Publication status
177    pub status: SketchStatus,
178    /// Domain this sketch belongs to (optional, string name)
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub domain: Option<String>,
181    /// Domain UUID reference (optional)
182    #[serde(skip_serializing_if = "Option::is_none", alias = "domain_id")]
183    pub domain_id: Option<Uuid>,
184    /// Workspace UUID reference (optional)
185    #[serde(skip_serializing_if = "Option::is_none", alias = "workspace_id")]
186    pub workspace_id: Option<Uuid>,
187
188    // Content
189    /// Brief description of the sketch
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub description: Option<String>,
192    /// Excalidraw scene data as JSON string
193    #[serde(alias = "excalidraw_data")]
194    pub excalidraw_data: String,
195    /// Optional path to PNG thumbnail (relative path, e.g., "thumbnails/sketch-0001.png")
196    #[serde(skip_serializing_if = "Option::is_none", alias = "thumbnail_path")]
197    pub thumbnail_path: Option<String>,
198
199    // Authorship
200    /// Sketch authors (emails or names)
201    #[serde(default, skip_serializing_if = "Vec::is_empty")]
202    pub authors: Vec<String>,
203
204    // Linking
205    /// Assets referenced by this sketch
206    #[serde(
207        default,
208        skip_serializing_if = "Vec::is_empty",
209        alias = "linked_assets"
210    )]
211    pub linked_assets: Vec<AssetLink>,
212    /// UUIDs of related decisions
213    #[serde(
214        default,
215        skip_serializing_if = "Vec::is_empty",
216        alias = "linked_decisions"
217    )]
218    pub linked_decisions: Vec<Uuid>,
219    /// UUIDs of related knowledge articles
220    #[serde(
221        default,
222        skip_serializing_if = "Vec::is_empty",
223        alias = "linked_knowledge"
224    )]
225    pub linked_knowledge: Vec<Uuid>,
226    /// UUIDs of related sketches
227    #[serde(
228        default,
229        skip_serializing_if = "Vec::is_empty",
230        alias = "related_sketches"
231    )]
232    pub related_sketches: Vec<Uuid>,
233
234    // Standard metadata
235    /// Tags for categorization
236    #[serde(default, skip_serializing_if = "Vec::is_empty")]
237    pub tags: Vec<Tag>,
238    /// Additional notes
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub notes: Option<String>,
241
242    /// Creation timestamp
243    #[serde(alias = "created_at")]
244    pub created_at: DateTime<Utc>,
245    /// Last modification timestamp
246    #[serde(alias = "updated_at")]
247    pub updated_at: DateTime<Utc>,
248}
249
250impl Sketch {
251    /// Create a new sketch with required fields
252    pub fn new(number: u64, title: impl Into<String>, excalidraw_data: impl Into<String>) -> Self {
253        let now = Utc::now();
254        Self {
255            id: Self::generate_id(number),
256            number,
257            title: title.into(),
258            sketch_type: SketchType::Architecture,
259            status: SketchStatus::Draft,
260            domain: None,
261            domain_id: None,
262            workspace_id: None,
263            description: None,
264            excalidraw_data: excalidraw_data.into(),
265            thumbnail_path: None,
266            authors: Vec::new(),
267            linked_assets: Vec::new(),
268            linked_decisions: Vec::new(),
269            linked_knowledge: Vec::new(),
270            related_sketches: Vec::new(),
271            tags: Vec::new(),
272            notes: None,
273            created_at: now,
274            updated_at: now,
275        }
276    }
277
278    /// Create a new sketch with a timestamp-based number (YYMMDDHHmm format)
279    /// This format prevents merge conflicts in distributed Git workflows
280    pub fn new_with_timestamp(
281        title: impl Into<String>,
282        excalidraw_data: impl Into<String>,
283    ) -> Self {
284        let now = Utc::now();
285        let number = Self::generate_timestamp_number(&now);
286        Self::new(number, title, excalidraw_data)
287    }
288
289    /// Generate a timestamp-based sketch number in YYMMDDHHmm format
290    pub fn generate_timestamp_number(dt: &DateTime<Utc>) -> u64 {
291        let formatted = dt.format("%y%m%d%H%M").to_string();
292        formatted.parse().unwrap_or(0)
293    }
294
295    /// Generate a deterministic UUID for a sketch based on its number
296    pub fn generate_id(number: u64) -> Uuid {
297        // Use UUID v5 with a namespace for sketches
298        let namespace = Uuid::parse_str("6ba7b810-9dad-11d1-80b4-00c04fd430c8").unwrap(); // URL namespace
299        let name = format!("sketch:{}", number);
300        Uuid::new_v5(&namespace, name.as_bytes())
301    }
302
303    /// Check if the sketch number is timestamp-based (YYMMDDHHmm format - 10 digits)
304    pub fn is_timestamp_number(&self) -> bool {
305        self.number >= 1000000000 && self.number <= 9999999999
306    }
307
308    /// Format the sketch number for display
309    /// Returns "SKETCH-0001" for sequential or "SKETCH-2601101234" for timestamp-based
310    pub fn formatted_number(&self) -> String {
311        if self.is_timestamp_number() {
312            format!("SKETCH-{}", self.number)
313        } else {
314            format!("SKETCH-{:04}", self.number)
315        }
316    }
317
318    /// Generate the YAML filename for this sketch
319    pub fn filename(&self, workspace_name: &str) -> String {
320        let number_str = if self.is_timestamp_number() {
321            format!("{}", self.number)
322        } else {
323            format!("{:04}", self.number)
324        };
325
326        match &self.domain {
327            Some(domain) => format!(
328                "{}_{}_sketch-{}.sketch.yaml",
329                sanitize_name(workspace_name),
330                sanitize_name(domain),
331                number_str
332            ),
333            None => format!(
334                "{}_sketch-{}.sketch.yaml",
335                sanitize_name(workspace_name),
336                number_str
337            ),
338        }
339    }
340
341    /// Generate the thumbnail filename for this sketch
342    pub fn thumbnail_filename(&self) -> String {
343        let number_str = if self.is_timestamp_number() {
344            format!("{}", self.number)
345        } else {
346            format!("{:04}", self.number)
347        };
348        format!("thumbnails/sketch-{}.png", number_str)
349    }
350
351    /// Set the sketch type
352    pub fn with_type(mut self, sketch_type: SketchType) -> Self {
353        self.sketch_type = sketch_type;
354        self.updated_at = Utc::now();
355        self
356    }
357
358    /// Set the sketch status
359    pub fn with_status(mut self, status: SketchStatus) -> Self {
360        self.status = status;
361        self.updated_at = Utc::now();
362        self
363    }
364
365    /// Set the domain
366    pub fn with_domain(mut self, domain: impl Into<String>) -> Self {
367        self.domain = Some(domain.into());
368        self.updated_at = Utc::now();
369        self
370    }
371
372    /// Set the domain ID
373    pub fn with_domain_id(mut self, domain_id: Uuid) -> Self {
374        self.domain_id = Some(domain_id);
375        self.updated_at = Utc::now();
376        self
377    }
378
379    /// Set the workspace ID
380    pub fn with_workspace_id(mut self, workspace_id: Uuid) -> Self {
381        self.workspace_id = Some(workspace_id);
382        self.updated_at = Utc::now();
383        self
384    }
385
386    /// Set the description
387    pub fn with_description(mut self, description: impl Into<String>) -> Self {
388        self.description = Some(description.into());
389        self.updated_at = Utc::now();
390        self
391    }
392
393    /// Set the thumbnail path
394    pub fn with_thumbnail(mut self, thumbnail_path: impl Into<String>) -> Self {
395        self.thumbnail_path = Some(thumbnail_path.into());
396        self.updated_at = Utc::now();
397        self
398    }
399
400    /// Add an author
401    pub fn add_author(mut self, author: impl Into<String>) -> Self {
402        self.authors.push(author.into());
403        self.updated_at = Utc::now();
404        self
405    }
406
407    /// Add an asset link
408    pub fn add_asset_link(mut self, link: AssetLink) -> Self {
409        self.linked_assets.push(link);
410        self.updated_at = Utc::now();
411        self
412    }
413
414    /// Link to a decision
415    pub fn link_decision(mut self, decision_id: Uuid) -> Self {
416        if !self.linked_decisions.contains(&decision_id) {
417            self.linked_decisions.push(decision_id);
418            self.updated_at = Utc::now();
419        }
420        self
421    }
422
423    /// Link to a knowledge article
424    pub fn link_knowledge(mut self, knowledge_id: Uuid) -> Self {
425        if !self.linked_knowledge.contains(&knowledge_id) {
426            self.linked_knowledge.push(knowledge_id);
427            self.updated_at = Utc::now();
428        }
429        self
430    }
431
432    /// Add a related sketch
433    pub fn add_related_sketch(mut self, sketch_id: Uuid) -> Self {
434        if !self.related_sketches.contains(&sketch_id) {
435            self.related_sketches.push(sketch_id);
436            self.updated_at = Utc::now();
437        }
438        self
439    }
440
441    /// Add a tag
442    pub fn add_tag(mut self, tag: Tag) -> Self {
443        self.tags.push(tag);
444        self.updated_at = Utc::now();
445        self
446    }
447
448    /// Set notes
449    pub fn with_notes(mut self, notes: impl Into<String>) -> Self {
450        self.notes = Some(notes.into());
451        self.updated_at = Utc::now();
452        self
453    }
454
455    /// Import from YAML
456    pub fn from_yaml(yaml_content: &str) -> Result<Self, serde_yaml::Error> {
457        serde_yaml::from_str(yaml_content)
458    }
459
460    /// Export to YAML
461    pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
462        serde_yaml::to_string(self)
463    }
464}
465
466/// Sketch index entry for the sketches.yaml file
467#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
468#[serde(rename_all = "camelCase")]
469pub struct SketchIndexEntry {
470    /// Sketch number (can be sequential or timestamp-based)
471    pub number: u64,
472    /// Sketch UUID
473    pub id: Uuid,
474    /// Sketch title
475    pub title: String,
476    /// Sketch type
477    #[serde(alias = "sketch_type")]
478    pub sketch_type: SketchType,
479    /// Sketch status
480    pub status: SketchStatus,
481    /// Domain (if applicable)
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub domain: Option<String>,
484    /// Filename of the sketch YAML file
485    pub file: String,
486    /// Optional thumbnail path
487    #[serde(skip_serializing_if = "Option::is_none", alias = "thumbnail_path")]
488    pub thumbnail_path: Option<String>,
489}
490
491impl From<&Sketch> for SketchIndexEntry {
492    fn from(sketch: &Sketch) -> Self {
493        Self {
494            number: sketch.number,
495            id: sketch.id,
496            title: sketch.title.clone(),
497            sketch_type: sketch.sketch_type.clone(),
498            status: sketch.status.clone(),
499            domain: sketch.domain.clone(),
500            file: String::new(), // Set by caller
501            thumbnail_path: sketch.thumbnail_path.clone(),
502        }
503    }
504}
505
506/// Sketch index (sketches.yaml)
507#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
508#[serde(rename_all = "camelCase")]
509pub struct SketchIndex {
510    /// Schema version
511    #[serde(alias = "schema_version")]
512    pub schema_version: String,
513    /// Last update timestamp
514    #[serde(skip_serializing_if = "Option::is_none", alias = "last_updated")]
515    pub last_updated: Option<DateTime<Utc>>,
516    /// List of sketches
517    #[serde(default)]
518    pub sketches: Vec<SketchIndexEntry>,
519    /// Next available sketch number (for sequential numbering)
520    #[serde(alias = "next_number")]
521    pub next_number: u64,
522    /// Whether to use timestamp-based numbering (YYMMDDHHmm format)
523    #[serde(default, alias = "use_timestamp_numbering")]
524    pub use_timestamp_numbering: bool,
525}
526
527impl Default for SketchIndex {
528    fn default() -> Self {
529        Self::new()
530    }
531}
532
533impl SketchIndex {
534    /// Create a new empty sketch index
535    pub fn new() -> Self {
536        Self {
537            schema_version: "1.0".to_string(),
538            last_updated: Some(Utc::now()),
539            sketches: Vec::new(),
540            next_number: 1,
541            use_timestamp_numbering: false,
542        }
543    }
544
545    /// Create a new sketch index with timestamp-based numbering
546    pub fn new_with_timestamp_numbering() -> Self {
547        Self {
548            schema_version: "1.0".to_string(),
549            last_updated: Some(Utc::now()),
550            sketches: Vec::new(),
551            next_number: 1,
552            use_timestamp_numbering: true,
553        }
554    }
555
556    /// Add a sketch to the index
557    pub fn add_sketch(&mut self, sketch: &Sketch, filename: String) {
558        let mut entry = SketchIndexEntry::from(sketch);
559        entry.file = filename;
560
561        // Remove existing entry with same number if present
562        self.sketches.retain(|s| s.number != sketch.number);
563        self.sketches.push(entry);
564
565        // Sort by number
566        self.sketches.sort_by(|a, b| a.number.cmp(&b.number));
567
568        // Update next number only for sequential numbering
569        if !self.use_timestamp_numbering && sketch.number >= self.next_number {
570            self.next_number = sketch.number + 1;
571        }
572
573        self.last_updated = Some(Utc::now());
574    }
575
576    /// Get the next available sketch number
577    /// For timestamp-based numbering, generates a new timestamp
578    /// For sequential numbering, returns the next sequential number
579    pub fn get_next_number(&self) -> u64 {
580        if self.use_timestamp_numbering {
581            Sketch::generate_timestamp_number(&Utc::now())
582        } else {
583            self.next_number
584        }
585    }
586
587    /// Find a sketch by number
588    pub fn find_by_number(&self, number: u64) -> Option<&SketchIndexEntry> {
589        self.sketches.iter().find(|s| s.number == number)
590    }
591
592    /// Import from YAML
593    pub fn from_yaml(yaml_content: &str) -> Result<Self, serde_yaml::Error> {
594        serde_yaml::from_str(yaml_content)
595    }
596
597    /// Export to YAML
598    pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
599        serde_yaml::to_string(self)
600    }
601}
602
603/// Sanitize a name for use in filenames
604fn sanitize_name(name: &str) -> String {
605    name.chars()
606        .map(|c| match c {
607            ' ' | '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '-',
608            _ => c,
609        })
610        .collect::<String>()
611        .to_lowercase()
612}
613
614#[cfg(test)]
615mod tests {
616    use super::*;
617
618    #[test]
619    fn test_sketch_new() {
620        let sketch = Sketch::new(1, "Architecture Diagram", "{}");
621
622        assert_eq!(sketch.number, 1);
623        assert_eq!(sketch.formatted_number(), "SKETCH-0001");
624        assert_eq!(sketch.title, "Architecture Diagram");
625        assert_eq!(sketch.status, SketchStatus::Draft);
626        assert_eq!(sketch.sketch_type, SketchType::Architecture);
627    }
628
629    #[test]
630    fn test_sketch_builder_pattern() {
631        let sketch = Sketch::new(1, "Test", "{}")
632            .with_type(SketchType::DataFlow)
633            .with_status(SketchStatus::Published)
634            .with_domain("sales")
635            .with_description("Test description")
636            .add_author("architect@example.com");
637
638        assert_eq!(sketch.sketch_type, SketchType::DataFlow);
639        assert_eq!(sketch.status, SketchStatus::Published);
640        assert_eq!(sketch.domain, Some("sales".to_string()));
641        assert_eq!(sketch.description, Some("Test description".to_string()));
642        assert_eq!(sketch.authors.len(), 1);
643    }
644
645    #[test]
646    fn test_sketch_id_generation() {
647        let id1 = Sketch::generate_id(1);
648        let id2 = Sketch::generate_id(1);
649        let id3 = Sketch::generate_id(2);
650
651        // Same number should generate same ID
652        assert_eq!(id1, id2);
653        // Different numbers should generate different IDs
654        assert_ne!(id1, id3);
655    }
656
657    #[test]
658    fn test_sketch_filename() {
659        let sketch = Sketch::new(1, "Test", "{}");
660        assert_eq!(
661            sketch.filename("enterprise"),
662            "enterprise_sketch-0001.sketch.yaml"
663        );
664
665        let sketch_with_domain = sketch.with_domain("sales");
666        assert_eq!(
667            sketch_with_domain.filename("enterprise"),
668            "enterprise_sales_sketch-0001.sketch.yaml"
669        );
670    }
671
672    #[test]
673    fn test_sketch_thumbnail_filename() {
674        let sketch = Sketch::new(1, "Test", "{}");
675        assert_eq!(sketch.thumbnail_filename(), "thumbnails/sketch-0001.png");
676
677        let timestamp_sketch = Sketch::new(2601101430, "Test", "{}");
678        assert_eq!(
679            timestamp_sketch.thumbnail_filename(),
680            "thumbnails/sketch-2601101430.png"
681        );
682    }
683
684    #[test]
685    fn test_sketch_yaml_roundtrip() {
686        let sketch = Sketch::new(1, "Test Sketch", r#"{"elements":[]}"#)
687            .with_status(SketchStatus::Published)
688            .with_domain("test");
689
690        let yaml = sketch.to_yaml().unwrap();
691        let parsed = Sketch::from_yaml(&yaml).unwrap();
692
693        assert_eq!(sketch.id, parsed.id);
694        assert_eq!(sketch.title, parsed.title);
695        assert_eq!(sketch.status, parsed.status);
696        assert_eq!(sketch.domain, parsed.domain);
697    }
698
699    #[test]
700    fn test_sketch_index() {
701        let mut index = SketchIndex::new();
702        assert_eq!(index.get_next_number(), 1);
703
704        let sketch1 = Sketch::new(1, "First", "{}");
705        index.add_sketch(&sketch1, "test_sketch-0001.sketch.yaml".to_string());
706
707        assert_eq!(index.sketches.len(), 1);
708        assert_eq!(index.get_next_number(), 2);
709
710        let sketch2 = Sketch::new(2, "Second", "{}");
711        index.add_sketch(&sketch2, "test_sketch-0002.sketch.yaml".to_string());
712
713        assert_eq!(index.sketches.len(), 2);
714        assert_eq!(index.get_next_number(), 3);
715    }
716
717    #[test]
718    fn test_sketch_type_display() {
719        assert_eq!(format!("{}", SketchType::Architecture), "Architecture");
720        assert_eq!(format!("{}", SketchType::DataFlow), "Data Flow");
721        assert_eq!(
722            format!("{}", SketchType::EntityRelationship),
723            "Entity Relationship"
724        );
725        assert_eq!(format!("{}", SketchType::Concept), "Concept");
726    }
727
728    #[test]
729    fn test_sketch_status_display() {
730        assert_eq!(format!("{}", SketchStatus::Draft), "Draft");
731        assert_eq!(format!("{}", SketchStatus::Review), "Review");
732        assert_eq!(format!("{}", SketchStatus::Published), "Published");
733        assert_eq!(format!("{}", SketchStatus::Archived), "Archived");
734    }
735
736    #[test]
737    fn test_timestamp_number_generation() {
738        use chrono::TimeZone;
739        let dt = Utc.with_ymd_and_hms(2026, 1, 10, 14, 30, 0).unwrap();
740        let number = Sketch::generate_timestamp_number(&dt);
741        assert_eq!(number, 2601101430);
742    }
743
744    #[test]
745    fn test_is_timestamp_number() {
746        let sequential_sketch = Sketch::new(1, "Test", "{}");
747        assert!(!sequential_sketch.is_timestamp_number());
748
749        let timestamp_sketch = Sketch::new(2601101430, "Test", "{}");
750        assert!(timestamp_sketch.is_timestamp_number());
751    }
752
753    #[test]
754    fn test_timestamp_sketch_filename() {
755        let sketch = Sketch::new(2601101430, "Test", "{}");
756        assert_eq!(
757            sketch.filename("enterprise"),
758            "enterprise_sketch-2601101430.sketch.yaml"
759        );
760    }
761
762    #[test]
763    fn test_sketch_index_with_timestamp_numbering() {
764        let index = SketchIndex::new_with_timestamp_numbering();
765        assert!(index.use_timestamp_numbering);
766
767        // The next number should be a timestamp
768        let next = index.get_next_number();
769        assert!(next >= 1000000000); // Timestamp format check
770    }
771
772    #[test]
773    fn test_sketch_linking() {
774        let decision_id = Uuid::new_v4();
775        let knowledge_id = Uuid::new_v4();
776        let sketch_id = Uuid::new_v4();
777
778        let sketch = Sketch::new(1, "Test", "{}")
779            .link_decision(decision_id)
780            .link_knowledge(knowledge_id)
781            .add_related_sketch(sketch_id);
782
783        assert_eq!(sketch.linked_decisions.len(), 1);
784        assert_eq!(sketch.linked_knowledge.len(), 1);
785        assert_eq!(sketch.related_sketches.len(), 1);
786    }
787}