Skip to main content

sara_core/model/
relationship.rs

1//! Relationship types and structures for the knowledge graph.
2
3use serde::{Deserialize, Serialize};
4
5use super::field::FieldName;
6use super::item::{ItemId, ItemType};
7
8/// Represents the type of relationship between items.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum RelationshipType {
12    /// Refinement: child refines parent (Scenario refines Use Case).
13    Refines,
14    /// Inverse of Refines: parent is refined by child.
15    IsRefinedBy,
16    /// Derivation: parent derives child (Scenario derives System Requirement).
17    Derives,
18    /// Inverse of Derives: child derives from parent.
19    DerivesFrom,
20    /// Satisfaction: child satisfies parent (System Architecture satisfies System Requirement).
21    Satisfies,
22    /// Inverse of Satisfies: parent is satisfied by child.
23    IsSatisfiedBy,
24    /// Dependency: Requirement depends on another Requirement of the same type.
25    DependsOn,
26    /// Inverse of DependsOn: Requirement is required by another.
27    IsRequiredBy,
28    /// Justification: ADR justifies a design artifact (SYSARCH, SWDD, HWDD).
29    Justifies,
30    /// Inverse of Justifies: design artifact is justified by an ADR.
31    IsJustifiedBy,
32    /// Supersession: newer ADR supersedes older ADR.
33    Supersedes,
34    /// Inverse of Supersedes: older ADR is superseded by newer ADR.
35    IsSupersededBy,
36}
37
38impl RelationshipType {
39    /// Get the inverse relationship type.
40    #[must_use]
41    pub const fn inverse(&self) -> Self {
42        match self {
43            Self::Refines => Self::IsRefinedBy,
44            Self::IsRefinedBy => Self::Refines,
45            Self::Derives => Self::DerivesFrom,
46            Self::DerivesFrom => Self::Derives,
47            Self::Satisfies => Self::IsSatisfiedBy,
48            Self::IsSatisfiedBy => Self::Satisfies,
49            Self::DependsOn => Self::IsRequiredBy,
50            Self::IsRequiredBy => Self::DependsOn,
51            Self::Justifies => Self::IsJustifiedBy,
52            Self::IsJustifiedBy => Self::Justifies,
53            Self::Supersedes => Self::IsSupersededBy,
54            Self::IsSupersededBy => Self::Supersedes,
55        }
56    }
57
58    /// Check if this is an upstream relationship (toward Solution).
59    /// For ADRs, Justifies is considered upstream as it links ADR to design artifacts.
60    #[must_use]
61    pub const fn is_upstream(&self) -> bool {
62        matches!(
63            self,
64            Self::Refines | Self::DerivesFrom | Self::Satisfies | Self::Justifies
65        )
66    }
67
68    /// Check if this is a downstream relationship (toward Detailed Designs).
69    #[must_use]
70    pub const fn is_downstream(&self) -> bool {
71        matches!(
72            self,
73            Self::IsRefinedBy | Self::Derives | Self::IsSatisfiedBy | Self::IsJustifiedBy
74        )
75    }
76
77    /// Check if this is a peer relationship (between items of the same type).
78    #[must_use]
79    pub const fn is_peer(&self) -> bool {
80        matches!(
81            self,
82            Self::DependsOn | Self::IsRequiredBy | Self::Supersedes | Self::IsSupersededBy
83        )
84    }
85
86    /// Check if this is a primary relationship (not an inverse).
87    ///
88    /// Primary relationships are the declared direction:
89    /// - Refines, DerivesFrom, Satisfies, Justifies (upstream)
90    /// - DependsOn, Supersedes (peer, primary)
91    ///
92    /// Inverse relationships exist only for graph traversal and should not
93    /// be considered when checking for cycles.
94    #[must_use]
95    pub const fn is_primary(&self) -> bool {
96        matches!(
97            self,
98            Self::Refines
99                | Self::DerivesFrom
100                | Self::Satisfies
101                | Self::Justifies
102                | Self::DependsOn
103                | Self::Supersedes
104        )
105    }
106
107    /// Returns the corresponding FieldName for this relationship type.
108    #[must_use]
109    pub const fn field_name(&self) -> FieldName {
110        match self {
111            Self::Refines => FieldName::Refines,
112            Self::IsRefinedBy => FieldName::IsRefinedBy,
113            Self::Derives => FieldName::Derives,
114            Self::DerivesFrom => FieldName::DerivesFrom,
115            Self::Satisfies => FieldName::Satisfies,
116            Self::IsSatisfiedBy => FieldName::IsSatisfiedBy,
117            Self::DependsOn => FieldName::DependsOn,
118            Self::IsRequiredBy => FieldName::IsRequiredBy,
119            Self::Justifies => FieldName::Justifies,
120            Self::IsJustifiedBy => FieldName::JustifiedBy,
121            Self::Supersedes => FieldName::Supersedes,
122            Self::IsSupersededBy => FieldName::SupersededBy,
123        }
124    }
125}
126
127impl std::fmt::Display for RelationshipType {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        write!(f, "{}", self.field_name().as_str())
130    }
131}
132
133/// Represents a relationship from an Item to another item.
134///
135/// The source item is implied by the `Item` containing this relationship.
136#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
137pub struct Relationship {
138    /// Target item ID.
139    pub to: ItemId,
140    /// Type of relationship.
141    pub relationship_type: RelationshipType,
142}
143
144impl Relationship {
145    /// Creates a new relationship to the given target.
146    #[must_use]
147    pub fn new(to: ItemId, relationship_type: RelationshipType) -> Self {
148        Self {
149            to,
150            relationship_type,
151        }
152    }
153
154    /// Returns the target item ID.
155    #[must_use]
156    pub fn target(&self) -> &ItemId {
157        &self.to
158    }
159
160    /// Returns the relationship type.
161    #[must_use]
162    pub fn rel_type(&self) -> RelationshipType {
163        self.relationship_type
164    }
165}
166
167/// Valid relationship rules based on item types.
168pub struct RelationshipRules;
169
170impl RelationshipRules {
171    /// Returns the valid upstream relationship types for a given item type.
172    #[must_use]
173    pub fn valid_upstream_for(item_type: ItemType) -> Option<(RelationshipType, Vec<ItemType>)> {
174        match item_type {
175            ItemType::Solution => None,
176            ItemType::UseCase => Some((RelationshipType::Refines, vec![ItemType::Solution])),
177            ItemType::Scenario => Some((RelationshipType::Refines, vec![ItemType::UseCase])),
178            ItemType::SystemRequirement => {
179                Some((RelationshipType::DerivesFrom, vec![ItemType::Scenario]))
180            }
181            ItemType::SystemArchitecture => Some((
182                RelationshipType::Satisfies,
183                vec![ItemType::SystemRequirement],
184            )),
185            ItemType::HardwareRequirement => Some((
186                RelationshipType::DerivesFrom,
187                vec![ItemType::SystemArchitecture],
188            )),
189            ItemType::SoftwareRequirement => Some((
190                RelationshipType::DerivesFrom,
191                vec![ItemType::SystemArchitecture],
192            )),
193            ItemType::HardwareDetailedDesign => Some((
194                RelationshipType::Satisfies,
195                vec![ItemType::HardwareRequirement],
196            )),
197            ItemType::SoftwareDetailedDesign => Some((
198                RelationshipType::Satisfies,
199                vec![ItemType::SoftwareRequirement],
200            )),
201            ItemType::ArchitectureDecisionRecord => Some((
202                RelationshipType::Justifies,
203                vec![
204                    ItemType::SystemArchitecture,
205                    ItemType::SoftwareDetailedDesign,
206                    ItemType::HardwareDetailedDesign,
207                ],
208            )),
209        }
210    }
211
212    /// Returns the valid downstream relationship types for a given item type.
213    #[must_use]
214    pub fn valid_downstream_for(item_type: ItemType) -> Option<(RelationshipType, Vec<ItemType>)> {
215        match item_type {
216            ItemType::Solution => Some((RelationshipType::IsRefinedBy, vec![ItemType::UseCase])),
217            ItemType::UseCase => Some((RelationshipType::IsRefinedBy, vec![ItemType::Scenario])),
218            ItemType::Scenario => {
219                Some((RelationshipType::Derives, vec![ItemType::SystemRequirement]))
220            }
221            ItemType::SystemRequirement => Some((
222                RelationshipType::IsSatisfiedBy,
223                vec![ItemType::SystemArchitecture],
224            )),
225            ItemType::SystemArchitecture => Some((
226                RelationshipType::Derives,
227                vec![ItemType::HardwareRequirement, ItemType::SoftwareRequirement],
228            )),
229            ItemType::HardwareRequirement => Some((
230                RelationshipType::IsSatisfiedBy,
231                vec![ItemType::HardwareDetailedDesign],
232            )),
233            ItemType::SoftwareRequirement => Some((
234                RelationshipType::IsSatisfiedBy,
235                vec![ItemType::SoftwareDetailedDesign],
236            )),
237            ItemType::HardwareDetailedDesign | ItemType::SoftwareDetailedDesign => Some((
238                RelationshipType::IsJustifiedBy,
239                vec![ItemType::ArchitectureDecisionRecord],
240            )),
241            ItemType::ArchitectureDecisionRecord => None,
242        }
243    }
244
245    /// Returns the valid peer dependency types for a given item type.
246    #[must_use]
247    pub const fn valid_peer_for(item_type: ItemType) -> Option<ItemType> {
248        match item_type {
249            ItemType::SystemRequirement => Some(ItemType::SystemRequirement),
250            ItemType::HardwareRequirement => Some(ItemType::HardwareRequirement),
251            ItemType::SoftwareRequirement => Some(ItemType::SoftwareRequirement),
252            ItemType::ArchitectureDecisionRecord => Some(ItemType::ArchitectureDecisionRecord),
253            _ => None,
254        }
255    }
256
257    /// Returns the valid justification targets for ADRs.
258    #[must_use]
259    pub fn valid_justification_targets() -> Vec<ItemType> {
260        vec![
261            ItemType::SystemArchitecture,
262            ItemType::SoftwareDetailedDesign,
263            ItemType::HardwareDetailedDesign,
264        ]
265    }
266
267    /// Checks if a justification relationship is valid (ADR -> design artifact).
268    #[must_use]
269    pub fn is_valid_justification(from_type: ItemType, to_type: ItemType) -> bool {
270        from_type == ItemType::ArchitectureDecisionRecord
271            && Self::valid_justification_targets().contains(&to_type)
272    }
273
274    /// Checks if a supersession relationship is valid (ADR -> ADR).
275    #[must_use]
276    pub const fn is_valid_supersession(from_type: ItemType, to_type: ItemType) -> bool {
277        matches!(from_type, ItemType::ArchitectureDecisionRecord)
278            && matches!(to_type, ItemType::ArchitectureDecisionRecord)
279    }
280
281    /// Checks if a relationship is valid between two item types.
282    #[must_use]
283    pub fn is_valid_relationship(
284        from_type: ItemType,
285        to_type: ItemType,
286        rel_type: RelationshipType,
287    ) -> bool {
288        match rel_type {
289            // Upstream relationships
290            RelationshipType::Refines
291            | RelationshipType::DerivesFrom
292            | RelationshipType::Satisfies
293            | RelationshipType::Justifies => {
294                if let Some((expected_rel, valid_targets)) = Self::valid_upstream_for(from_type) {
295                    expected_rel == rel_type && valid_targets.contains(&to_type)
296                } else {
297                    false
298                }
299            }
300            // Downstream relationships
301            RelationshipType::IsRefinedBy
302            | RelationshipType::Derives
303            | RelationshipType::IsSatisfiedBy => {
304                if let Some((expected_rel, valid_targets)) = Self::valid_downstream_for(from_type) {
305                    expected_rel == rel_type && valid_targets.contains(&to_type)
306                } else {
307                    false
308                }
309            }
310            // IsJustifiedBy needs special handling since design artifacts have multiple downstream types
311            RelationshipType::IsJustifiedBy => Self::is_valid_justification(to_type, from_type),
312            // Peer relationships (including ADR supersession)
313            RelationshipType::DependsOn
314            | RelationshipType::IsRequiredBy
315            | RelationshipType::Supersedes
316            | RelationshipType::IsSupersededBy => Self::valid_peer_for(from_type) == Some(to_type),
317        }
318    }
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    #[test]
326    fn test_relationship_type_inverse() {
327        assert_eq!(
328            RelationshipType::Refines.inverse(),
329            RelationshipType::IsRefinedBy
330        );
331        assert_eq!(
332            RelationshipType::Derives.inverse(),
333            RelationshipType::DerivesFrom
334        );
335        assert_eq!(
336            RelationshipType::Satisfies.inverse(),
337            RelationshipType::IsSatisfiedBy
338        );
339        assert_eq!(
340            RelationshipType::DependsOn.inverse(),
341            RelationshipType::IsRequiredBy
342        );
343    }
344
345    #[test]
346    fn test_relationship_type_direction() {
347        assert!(RelationshipType::Refines.is_upstream());
348        assert!(RelationshipType::DerivesFrom.is_upstream());
349        assert!(RelationshipType::Satisfies.is_upstream());
350
351        assert!(RelationshipType::IsRefinedBy.is_downstream());
352        assert!(RelationshipType::Derives.is_downstream());
353        assert!(RelationshipType::IsSatisfiedBy.is_downstream());
354
355        assert!(RelationshipType::DependsOn.is_peer());
356        assert!(RelationshipType::IsRequiredBy.is_peer());
357    }
358
359    #[test]
360    fn test_valid_relationships() {
361        // UseCase refines Solution
362        assert!(RelationshipRules::is_valid_relationship(
363            ItemType::UseCase,
364            ItemType::Solution,
365            RelationshipType::Refines
366        ));
367
368        // Scenario refines UseCase
369        assert!(RelationshipRules::is_valid_relationship(
370            ItemType::Scenario,
371            ItemType::UseCase,
372            RelationshipType::Refines
373        ));
374
375        // SystemRequirement derives_from Scenario
376        assert!(RelationshipRules::is_valid_relationship(
377            ItemType::SystemRequirement,
378            ItemType::Scenario,
379            RelationshipType::DerivesFrom
380        ));
381
382        // Invalid: Solution refines nothing
383        assert!(!RelationshipRules::is_valid_relationship(
384            ItemType::Solution,
385            ItemType::UseCase,
386            RelationshipType::Refines
387        ));
388    }
389
390    #[test]
391    fn test_peer_dependencies() {
392        assert!(RelationshipRules::is_valid_relationship(
393            ItemType::SystemRequirement,
394            ItemType::SystemRequirement,
395            RelationshipType::DependsOn
396        ));
397
398        assert!(!RelationshipRules::is_valid_relationship(
399            ItemType::Solution,
400            ItemType::Solution,
401            RelationshipType::DependsOn
402        ));
403    }
404
405    #[test]
406    fn test_adr_justifies_relationship() {
407        // ADR can justify design artifacts
408        assert!(RelationshipRules::is_valid_relationship(
409            ItemType::ArchitectureDecisionRecord,
410            ItemType::SystemArchitecture,
411            RelationshipType::Justifies
412        ));
413        assert!(RelationshipRules::is_valid_relationship(
414            ItemType::ArchitectureDecisionRecord,
415            ItemType::SoftwareDetailedDesign,
416            RelationshipType::Justifies
417        ));
418        assert!(RelationshipRules::is_valid_relationship(
419            ItemType::ArchitectureDecisionRecord,
420            ItemType::HardwareDetailedDesign,
421            RelationshipType::Justifies
422        ));
423
424        // ADR cannot justify non-design artifacts
425        assert!(!RelationshipRules::is_valid_relationship(
426            ItemType::ArchitectureDecisionRecord,
427            ItemType::SystemRequirement,
428            RelationshipType::Justifies
429        ));
430    }
431
432    #[test]
433    fn test_adr_supersession_relationship() {
434        // ADR can supersede other ADRs (peer relationship)
435        assert!(RelationshipRules::is_valid_relationship(
436            ItemType::ArchitectureDecisionRecord,
437            ItemType::ArchitectureDecisionRecord,
438            RelationshipType::Supersedes
439        ));
440        assert!(RelationshipRules::is_valid_relationship(
441            ItemType::ArchitectureDecisionRecord,
442            ItemType::ArchitectureDecisionRecord,
443            RelationshipType::IsSupersededBy
444        ));
445
446        // ADR cannot supersede non-ADR items
447        assert!(!RelationshipRules::is_valid_relationship(
448            ItemType::ArchitectureDecisionRecord,
449            ItemType::SystemArchitecture,
450            RelationshipType::Supersedes
451        ));
452    }
453
454    #[test]
455    fn test_adr_relationship_direction() {
456        // Justifies is upstream
457        assert!(RelationshipType::Justifies.is_upstream());
458        // IsJustifiedBy is downstream
459        assert!(RelationshipType::IsJustifiedBy.is_downstream());
460        // Supersedes/IsSupersededBy are peer
461        assert!(RelationshipType::Supersedes.is_peer());
462        assert!(RelationshipType::IsSupersededBy.is_peer());
463    }
464}