Skip to main content

sara_core/model/
item.rs

1//! Item types and structures for the knowledge graph.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6use crate::error::SaraError;
7use crate::model::FieldName;
8use crate::model::relationship::{Relationship, RelationshipType};
9
10use super::adr::AdrStatus;
11
12/// Represents the type of item in the knowledge graph.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ItemType {
16    Solution,
17    UseCase,
18    Scenario,
19    SystemRequirement,
20    SystemArchitecture,
21    HardwareRequirement,
22    SoftwareRequirement,
23    HardwareDetailedDesign,
24    SoftwareDetailedDesign,
25    ArchitectureDecisionRecord,
26}
27
28impl ItemType {
29    /// Returns all item types in hierarchy order (upstream to downstream).
30    #[must_use]
31    pub const fn all() -> &'static [ItemType] {
32        &[
33            Self::Solution,
34            Self::UseCase,
35            Self::Scenario,
36            Self::SystemRequirement,
37            Self::SystemArchitecture,
38            Self::HardwareRequirement,
39            Self::SoftwareRequirement,
40            Self::HardwareDetailedDesign,
41            Self::SoftwareDetailedDesign,
42            Self::ArchitectureDecisionRecord,
43        ]
44    }
45
46    /// Returns the display name for this item type.
47    #[must_use]
48    pub const fn display_name(&self) -> &'static str {
49        match self {
50            Self::Solution => "Solution",
51            Self::UseCase => "Use Case",
52            Self::Scenario => "Scenario",
53            Self::SystemRequirement => "System Requirement",
54            Self::SystemArchitecture => "System Architecture",
55            Self::HardwareRequirement => "Hardware Requirement",
56            Self::SoftwareRequirement => "Software Requirement",
57            Self::HardwareDetailedDesign => "Hardware Detailed Design",
58            Self::SoftwareDetailedDesign => "Software Detailed Design",
59            Self::ArchitectureDecisionRecord => "Architecture Decision Record",
60        }
61    }
62
63    /// Returns the common ID prefix for this item type.
64    #[must_use]
65    pub const fn prefix(&self) -> &'static str {
66        match self {
67            Self::Solution => "SOL",
68            Self::UseCase => "UC",
69            Self::Scenario => "SCEN",
70            Self::SystemRequirement => "SYSREQ",
71            Self::SystemArchitecture => "SYSARCH",
72            Self::HardwareRequirement => "HWREQ",
73            Self::SoftwareRequirement => "SWREQ",
74            Self::HardwareDetailedDesign => "HWDD",
75            Self::SoftwareDetailedDesign => "SWDD",
76            Self::ArchitectureDecisionRecord => "ADR",
77        }
78    }
79
80    /// Generates a new ID for the given type with an optional sequence number.
81    ///
82    /// Defaults to sequence 1 if not provided.
83    #[must_use]
84    pub fn generate_id(&self, sequence: Option<u32>) -> String {
85        let num = sequence.unwrap_or(1);
86        format!("{}-{:03}", self.prefix(), num)
87    }
88
89    /// Suggests the next sequential ID based on existing items in the graph.
90    ///
91    /// Finds the highest existing ID for this type and returns the next one.
92    /// If no graph is provided or no items exist, returns the first ID (e.g., "SOL-001").
93    #[must_use]
94    pub fn suggest_next_id(&self, graph: Option<&crate::graph::KnowledgeGraph>) -> String {
95        let Some(graph) = graph else {
96            return self.generate_id(None);
97        };
98
99        let prefix = self.prefix();
100        let max_num = graph
101            .items()
102            .filter(|item| item.item_type == *self)
103            .filter_map(|item| {
104                item.id
105                    .as_str()
106                    .strip_prefix(prefix)
107                    .and_then(|suffix| suffix.trim_start_matches('-').parse::<u32>().ok())
108            })
109            .max()
110            .unwrap_or(0);
111
112        format!("{}-{:03}", prefix, max_num + 1)
113    }
114
115    /// Returns true if this item type requires the refines field.
116    #[must_use]
117    pub const fn requires_refines(&self) -> bool {
118        matches!(self, Self::UseCase | Self::Scenario)
119    }
120
121    /// Returns true if this item type requires the derives_from field.
122    #[must_use]
123    pub const fn requires_derives_from(&self) -> bool {
124        matches!(
125            self,
126            Self::SystemRequirement | Self::HardwareRequirement | Self::SoftwareRequirement
127        )
128    }
129
130    /// Returns true if this item type requires the satisfies field.
131    #[must_use]
132    pub const fn requires_satisfies(&self) -> bool {
133        matches!(
134            self,
135            Self::SystemArchitecture | Self::HardwareDetailedDesign | Self::SoftwareDetailedDesign
136        )
137    }
138
139    /// Returns true if this item type requires/accepts a specification field.
140    #[must_use]
141    pub const fn requires_specification(&self) -> bool {
142        matches!(
143            self,
144            Self::SystemRequirement | Self::HardwareRequirement | Self::SoftwareRequirement
145        )
146    }
147
148    /// Returns true if this item type accepts the platform field.
149    #[must_use]
150    pub const fn accepts_platform(&self) -> bool {
151        matches!(self, Self::SystemArchitecture)
152    }
153
154    /// Returns true if this item type accepts the depends_on field (peer dependencies).
155    #[must_use]
156    pub const fn supports_depends_on(&self) -> bool {
157        matches!(
158            self,
159            Self::SystemRequirement | Self::HardwareRequirement | Self::SoftwareRequirement
160        )
161    }
162
163    /// Returns true if this is a root item type (Solution).
164    #[must_use]
165    pub const fn is_root(&self) -> bool {
166        matches!(self, Self::Solution)
167    }
168
169    /// Returns true if this is an Architecture Decision Record type.
170    #[must_use]
171    pub const fn requires_deciders(&self) -> bool {
172        matches!(self, Self::ArchitectureDecisionRecord)
173    }
174
175    /// Returns true if this item type supports the status field (ADR only).
176    #[must_use]
177    pub const fn supports_status(&self) -> bool {
178        matches!(self, Self::ArchitectureDecisionRecord)
179    }
180
181    /// Returns true if this item type supports the supersedes field (ADR peer relationship).
182    #[must_use]
183    pub const fn supports_supersedes(&self) -> bool {
184        matches!(self, Self::ArchitectureDecisionRecord)
185    }
186
187    /// Returns the required parent item type for this type, if any.
188    ///
189    /// Solution has no parent (root of the hierarchy).
190    #[must_use]
191    pub const fn required_parent_type(&self) -> Option<ItemType> {
192        match self {
193            Self::Solution => None,
194            Self::UseCase => Some(Self::Solution),
195            Self::Scenario => Some(Self::UseCase),
196            Self::SystemRequirement => Some(Self::Scenario),
197            Self::SystemArchitecture => Some(Self::SystemRequirement),
198            Self::HardwareRequirement => Some(Self::SystemArchitecture),
199            Self::SoftwareRequirement => Some(Self::SystemArchitecture),
200            Self::HardwareDetailedDesign => Some(Self::HardwareRequirement),
201            Self::SoftwareDetailedDesign => Some(Self::SoftwareRequirement),
202            Self::ArchitectureDecisionRecord => None,
203        }
204    }
205
206    /// Returns the upstream traceability field for this item type.
207    #[must_use]
208    pub const fn traceability_field(&self) -> Option<FieldName> {
209        match self {
210            Self::Solution => None,
211            Self::UseCase | Self::Scenario => Some(FieldName::Refines),
212            Self::SystemRequirement | Self::HardwareRequirement | Self::SoftwareRequirement => {
213                Some(FieldName::DerivesFrom)
214            }
215            Self::SystemArchitecture
216            | Self::HardwareDetailedDesign
217            | Self::SoftwareDetailedDesign => Some(FieldName::Satisfies),
218            Self::ArchitectureDecisionRecord => Some(FieldName::Justifies),
219        }
220    }
221
222    /// Returns the YAML value (snake_case string) for this item type.
223    #[must_use]
224    pub const fn as_str(&self) -> &'static str {
225        match self {
226            Self::Solution => "solution",
227            Self::UseCase => "use_case",
228            Self::Scenario => "scenario",
229            Self::SystemRequirement => "system_requirement",
230            Self::SystemArchitecture => "system_architecture",
231            Self::HardwareRequirement => "hardware_requirement",
232            Self::SoftwareRequirement => "software_requirement",
233            Self::HardwareDetailedDesign => "hardware_detailed_design",
234            Self::SoftwareDetailedDesign => "software_detailed_design",
235            Self::ArchitectureDecisionRecord => "architecture_decision_record",
236        }
237    }
238
239    /// Returns all traceability configurations for this item type.
240    ///
241    /// Most item types have a single traceability link (e.g., refines, satisfies).
242    /// Requirement types have two: derives_from (hierarchical) and depends_on (peer).
243    /// Solution has no parent and returns an empty vec.
244    #[must_use]
245    pub fn traceability_configs(&self) -> Vec<TraceabilityConfig> {
246        match self {
247            ItemType::Solution => vec![],
248            ItemType::UseCase => vec![TraceabilityConfig {
249                relationship_field: FieldName::Refines,
250                target_type: ItemType::Solution,
251            }],
252            ItemType::Scenario => vec![TraceabilityConfig {
253                relationship_field: FieldName::Refines,
254                target_type: ItemType::UseCase,
255            }],
256            ItemType::SystemRequirement => vec![
257                TraceabilityConfig {
258                    relationship_field: FieldName::DerivesFrom,
259                    target_type: ItemType::Scenario,
260                },
261                TraceabilityConfig {
262                    relationship_field: FieldName::DependsOn,
263                    target_type: ItemType::SystemRequirement,
264                },
265            ],
266            ItemType::SystemArchitecture => vec![TraceabilityConfig {
267                relationship_field: FieldName::Satisfies,
268                target_type: ItemType::SystemRequirement,
269            }],
270            ItemType::HardwareRequirement => vec![
271                TraceabilityConfig {
272                    relationship_field: FieldName::DerivesFrom,
273                    target_type: ItemType::SystemArchitecture,
274                },
275                TraceabilityConfig {
276                    relationship_field: FieldName::DependsOn,
277                    target_type: ItemType::HardwareRequirement,
278                },
279            ],
280            ItemType::SoftwareRequirement => vec![
281                TraceabilityConfig {
282                    relationship_field: FieldName::DerivesFrom,
283                    target_type: ItemType::SystemArchitecture,
284                },
285                TraceabilityConfig {
286                    relationship_field: FieldName::DependsOn,
287                    target_type: ItemType::SoftwareRequirement,
288                },
289            ],
290            ItemType::HardwareDetailedDesign => vec![TraceabilityConfig {
291                relationship_field: FieldName::Satisfies,
292                target_type: ItemType::HardwareRequirement,
293            }],
294            ItemType::SoftwareDetailedDesign => vec![TraceabilityConfig {
295                relationship_field: FieldName::Satisfies,
296                target_type: ItemType::SoftwareRequirement,
297            }],
298            ItemType::ArchitectureDecisionRecord => vec![
299                TraceabilityConfig {
300                    relationship_field: FieldName::Justifies,
301                    target_type: ItemType::SystemArchitecture,
302                },
303                TraceabilityConfig {
304                    relationship_field: FieldName::Justifies,
305                    target_type: ItemType::SoftwareDetailedDesign,
306                },
307                TraceabilityConfig {
308                    relationship_field: FieldName::Justifies,
309                    target_type: ItemType::HardwareDetailedDesign,
310                },
311            ],
312        }
313    }
314}
315
316/// Configuration for traceability relationships.
317#[derive(Debug, Clone, Copy, PartialEq, Eq)]
318pub struct TraceabilityConfig {
319    /// The relationship field (refines, derives_from, satisfies, depends_on).
320    pub relationship_field: FieldName,
321    /// The target item type to link to (parent for hierarchical, same type for peers).
322    pub target_type: ItemType,
323}
324
325impl fmt::Display for ItemType {
326    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327        write!(f, "{}", self.display_name())
328    }
329}
330
331/// Unique identifier for an item across all repositories.
332#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
333#[serde(transparent)]
334pub struct ItemId(String);
335
336impl ItemId {
337    /// Creates a new ItemId, validating format.
338    pub fn new(id: impl Into<String>) -> Result<Self, SaraError> {
339        let id = id.into();
340        if id.is_empty() {
341            return Err(SaraError::InvalidId {
342                id: id.clone(),
343                reason: "Item ID cannot be empty".to_string(),
344            });
345        }
346
347        // Validate: alphanumeric, hyphens, and underscores only
348        if !id
349            .chars()
350            .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
351        {
352            return Err(SaraError::InvalidId {
353                id: id.clone(),
354                reason:
355                    "Item ID must contain only alphanumeric characters, hyphens, and underscores"
356                        .to_string(),
357            });
358        }
359
360        Ok(Self(id))
361    }
362
363    /// Creates a new ItemId without validation.
364    ///
365    /// Use this when parsing from trusted sources where IDs have already been
366    /// validated or when the ID format is known to be valid.
367    pub fn new_unchecked(id: impl Into<String>) -> Self {
368        Self(id.into())
369    }
370
371    /// Returns the raw identifier string.
372    #[must_use]
373    pub fn as_str(&self) -> &str {
374        &self.0
375    }
376}
377
378impl fmt::Display for ItemId {
379    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
380        write!(f, "{}", self.0)
381    }
382}
383
384impl AsRef<str> for ItemId {
385    fn as_ref(&self) -> &str {
386        &self.0
387    }
388}
389
390/// Type-specific attributes for items in the knowledge graph.
391#[derive(Debug, Clone, Default, Serialize, Deserialize)]
392#[serde(tag = "_attr_type")]
393pub enum ItemAttributes {
394    /// Solution - no type-specific attributes.
395    #[serde(rename = "solution")]
396    #[default]
397    Solution,
398
399    /// Use Case - no type-specific attributes beyond upstream refs.
400    #[serde(rename = "use_case")]
401    UseCase,
402
403    /// Scenario - no type-specific attributes beyond upstream refs.
404    #[serde(rename = "scenario")]
405    Scenario,
406
407    /// System Requirement with specification and peer dependencies.
408    #[serde(rename = "system_requirement")]
409    SystemRequirement {
410        /// Specification statement.
411        specification: String,
412        /// Peer dependencies.
413        #[serde(default, skip_serializing_if = "Vec::is_empty")]
414        depends_on: Vec<ItemId>,
415    },
416
417    /// System Architecture with platform.
418    #[serde(rename = "system_architecture")]
419    SystemArchitecture {
420        /// Target platform.
421        #[serde(default, skip_serializing_if = "Option::is_none")]
422        platform: Option<String>,
423    },
424
425    /// Software Requirement with specification and peer dependencies.
426    #[serde(rename = "software_requirement")]
427    SoftwareRequirement {
428        /// Specification statement.
429        specification: String,
430        /// Peer dependencies.
431        #[serde(default, skip_serializing_if = "Vec::is_empty")]
432        depends_on: Vec<ItemId>,
433    },
434
435    /// Hardware Requirement with specification and peer dependencies.
436    #[serde(rename = "hardware_requirement")]
437    HardwareRequirement {
438        /// Specification statement.
439        specification: String,
440        /// Peer dependencies.
441        #[serde(default, skip_serializing_if = "Vec::is_empty")]
442        depends_on: Vec<ItemId>,
443    },
444
445    /// Software Detailed Design.
446    #[serde(rename = "software_detailed_design")]
447    SoftwareDetailedDesign,
448
449    /// Hardware Detailed Design.
450    #[serde(rename = "hardware_detailed_design")]
451    HardwareDetailedDesign,
452
453    /// Architecture Decision Record with ADR-specific fields.
454    #[serde(rename = "architecture_decision_record")]
455    Adr {
456        /// ADR lifecycle status.
457        status: AdrStatus,
458        /// List of people involved in the decision.
459        deciders: Vec<String>,
460        /// Older ADRs this decision supersedes (peer relationship).
461        #[serde(default, skip_serializing_if = "Vec::is_empty")]
462        supersedes: Vec<ItemId>,
463    },
464}
465
466impl ItemAttributes {
467    /// Creates default attributes for the given item type.
468    #[must_use]
469    pub fn for_type(item_type: ItemType) -> Self {
470        match item_type {
471            ItemType::Solution => ItemAttributes::Solution,
472            ItemType::UseCase => ItemAttributes::UseCase,
473            ItemType::Scenario => ItemAttributes::Scenario,
474            ItemType::SystemRequirement => ItemAttributes::SystemRequirement {
475                specification: String::new(),
476                depends_on: Vec::new(),
477            },
478            ItemType::SystemArchitecture => ItemAttributes::SystemArchitecture { platform: None },
479            ItemType::SoftwareRequirement => ItemAttributes::SoftwareRequirement {
480                specification: String::new(),
481                depends_on: Vec::new(),
482            },
483            ItemType::HardwareRequirement => ItemAttributes::HardwareRequirement {
484                specification: String::new(),
485                depends_on: Vec::new(),
486            },
487            ItemType::SoftwareDetailedDesign => ItemAttributes::SoftwareDetailedDesign,
488            ItemType::HardwareDetailedDesign => ItemAttributes::HardwareDetailedDesign,
489            ItemType::ArchitectureDecisionRecord => ItemAttributes::Adr {
490                status: AdrStatus::Proposed,
491                deciders: Vec::new(),
492                supersedes: Vec::new(),
493            },
494        }
495    }
496
497    /// Returns the specification if this is a requirement type.
498    #[must_use]
499    pub fn specification(&self) -> Option<&String> {
500        match self {
501            Self::SystemRequirement { specification, .. }
502            | Self::SoftwareRequirement { specification, .. }
503            | Self::HardwareRequirement { specification, .. } => Some(specification),
504            _ => None,
505        }
506    }
507
508    /// Returns the depends_on references if this is a requirement type.
509    #[must_use]
510    pub fn depends_on(&self) -> &[ItemId] {
511        match self {
512            Self::SystemRequirement { depends_on, .. }
513            | Self::SoftwareRequirement { depends_on, .. }
514            | Self::HardwareRequirement { depends_on, .. } => depends_on,
515            _ => &[],
516        }
517    }
518
519    /// Returns the platform if this is a SystemArchitecture.
520    #[must_use]
521    pub fn platform(&self) -> Option<&String> {
522        match self {
523            Self::SystemArchitecture { platform, .. } => platform.as_ref(),
524            _ => None,
525        }
526    }
527
528    /// Returns the ADR status if this is an ADR.
529    #[must_use]
530    pub fn status(&self) -> Option<AdrStatus> {
531        match self {
532            Self::Adr { status, .. } => Some(*status),
533            _ => None,
534        }
535    }
536
537    /// Returns the deciders if this is an ADR.
538    #[must_use]
539    pub fn deciders(&self) -> &[String] {
540        match self {
541            Self::Adr { deciders, .. } => deciders,
542            _ => &[],
543        }
544    }
545
546    /// Returns the supersedes references if this is an ADR.
547    #[must_use]
548    pub fn supersedes(&self) -> &[ItemId] {
549        match self {
550            Self::Adr { supersedes, .. } => supersedes,
551            _ => &[],
552        }
553    }
554}
555
556use crate::model::metadata::SourceLocation;
557
558/// Represents a single document/node in the knowledge graph.
559#[derive(Debug, Clone, Serialize, Deserialize)]
560pub struct Item {
561    /// Unique identifier.
562    pub id: ItemId,
563
564    /// Type of this item.
565    pub item_type: ItemType,
566
567    /// Human-readable name.
568    pub name: String,
569
570    /// Optional description.
571    #[serde(default, skip_serializing_if = "Option::is_none")]
572    pub description: Option<String>,
573
574    /// Source file location.
575    pub source: SourceLocation,
576
577    /// All relationships from this item to other items.
578    #[serde(default)]
579    pub relationships: Vec<Relationship>,
580
581    /// Type-specific attributes.
582    #[serde(default)]
583    pub attributes: ItemAttributes,
584}
585
586impl Item {
587    /// Returns an iterator over target IDs for a specific relationship type.
588    pub fn relationship_ids(&self, rel_type: RelationshipType) -> impl Iterator<Item = &ItemId> {
589        self.relationships
590            .iter()
591            .filter(move |r| r.relationship_type == rel_type)
592            .map(|r| &r.to)
593    }
594
595    /// Returns true if this item has any relationships of the given type.
596    #[must_use]
597    pub fn has_relationship_type(&self, rel_type: RelationshipType) -> bool {
598        self.relationships
599            .iter()
600            .any(|r| r.relationship_type == rel_type)
601    }
602
603    /// Returns true if this item has any upstream relationships.
604    #[must_use]
605    pub fn has_upstream(&self) -> bool {
606        self.relationships
607            .iter()
608            .any(|r| r.relationship_type.is_upstream())
609    }
610
611    /// Returns an iterator over all referenced item IDs (relationships and peer refs from attributes).
612    pub fn all_references(&self) -> impl Iterator<Item = &ItemId> {
613        let relationship_refs = self.relationships.iter().map(|r| &r.to);
614
615        // Peer references from attributes (depends_on for requirements, supersedes for ADRs)
616        let peer_refs: Box<dyn Iterator<Item = &ItemId>> = match &self.attributes {
617            ItemAttributes::SystemRequirement { depends_on, .. }
618            | ItemAttributes::SoftwareRequirement { depends_on, .. }
619            | ItemAttributes::HardwareRequirement { depends_on, .. } => Box::new(depends_on.iter()),
620            ItemAttributes::Adr { supersedes, .. } => Box::new(supersedes.iter()),
621            _ => Box::new(std::iter::empty()),
622        };
623
624        relationship_refs.chain(peer_refs)
625    }
626}
627
628#[cfg(test)]
629mod tests {
630    use super::*;
631
632    #[test]
633    fn test_item_id_valid() {
634        assert!(ItemId::new("SOL-001").is_ok());
635        assert!(ItemId::new("UC_002").is_ok());
636        assert!(ItemId::new("SYSREQ-123-A").is_ok());
637    }
638
639    #[test]
640    fn test_item_id_invalid() {
641        assert!(ItemId::new("").is_err());
642        assert!(ItemId::new("SOL 001").is_err());
643        assert!(ItemId::new("SOL.001").is_err());
644    }
645
646    #[test]
647    fn test_item_type_display() {
648        assert_eq!(ItemType::Solution.display_name(), "Solution");
649        assert_eq!(
650            ItemType::SystemRequirement.display_name(),
651            "System Requirement"
652        );
653    }
654
655    #[test]
656    fn test_item_type_requires_specification() {
657        assert!(ItemType::SystemRequirement.requires_specification());
658        assert!(ItemType::HardwareRequirement.requires_specification());
659        assert!(ItemType::SoftwareRequirement.requires_specification());
660        assert!(!ItemType::Solution.requires_specification());
661        assert!(!ItemType::Scenario.requires_specification());
662    }
663
664    #[test]
665    fn test_generate_id() {
666        assert_eq!(ItemType::Solution.generate_id(Some(1)), "SOL-001");
667        assert_eq!(ItemType::UseCase.generate_id(Some(42)), "UC-042");
668        assert_eq!(ItemType::SystemRequirement.generate_id(None), "SYSREQ-001");
669    }
670}