sea_core/
sbvr.rs

1use crate::graph::Graph;
2use serde::{Deserialize, Serialize};
3use std::collections::HashSet;
4
5#[derive(Debug, Clone)]
6pub enum SbvrError {
7    SerializationError(String),
8    UnsupportedConstruct(String),
9}
10
11impl std::fmt::Display for SbvrError {
12    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13        match self {
14            SbvrError::SerializationError(msg) => write!(f, "SBVR serialization error: {}", msg),
15            SbvrError::UnsupportedConstruct(msg) => write!(f, "Unsupported construct: {}", msg),
16        }
17    }
18}
19
20impl std::error::Error for SbvrError {}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct SbvrTerm {
24    pub id: String,
25    pub name: String,
26    pub term_type: TermType,
27    pub definition: Option<String>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub enum TermType {
32    GeneralConcept,
33    IndividualConcept,
34    VerbConcept,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct SbvrFactType {
39    pub id: String,
40    pub subject: String,
41    pub verb: String,
42    pub object: String,
43    #[serde(default)]
44    pub destination: Option<String>,
45    #[serde(default = "SbvrFactType::default_schema_version")]
46    pub schema_version: String,
47}
48
49impl SbvrFactType {
50    pub fn default_schema_version() -> String {
51        "2.0".to_string()
52    }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct SbvrBusinessRule {
57    pub id: String,
58    pub name: String,
59    pub rule_type: RuleType,
60    pub expression: String,
61    pub severity: String,
62    #[serde(default)]
63    pub priority: Option<u8>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub enum RuleType {
68    Obligation,
69    Prohibition,
70    Permission,
71    Derivation,
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct SbvrModel {
76    pub vocabulary: Vec<SbvrTerm>,
77    pub facts: Vec<SbvrFactType>,
78    pub rules: Vec<SbvrBusinessRule>,
79}
80
81impl SbvrModel {
82    pub fn new() -> Self {
83        Self {
84            vocabulary: Vec::new(),
85            facts: Vec::new(),
86            rules: Vec::new(),
87        }
88    }
89
90    pub fn from_graph(graph: &Graph) -> Result<Self, SbvrError> {
91        let mut model = Self::new();
92        let mut relation_predicates: HashSet<String> = HashSet::new();
93
94        for entity in graph.all_entities() {
95            model.vocabulary.push(SbvrTerm {
96                id: entity.id().to_string(),
97                name: entity.name().to_string(),
98                term_type: TermType::GeneralConcept,
99                definition: Some(format!("Entity: {}", entity.name())),
100            });
101        }
102
103        for role in graph.all_roles() {
104            model.vocabulary.push(SbvrTerm {
105                id: role.id().to_string(),
106                name: role.name().to_string(),
107                term_type: TermType::GeneralConcept,
108                definition: Some(format!("Role: {}", role.name())),
109            });
110        }
111
112        for resource in graph.all_resources() {
113            model.vocabulary.push(SbvrTerm {
114                id: resource.id().to_string(),
115                name: resource.name().to_string(),
116                term_type: TermType::IndividualConcept,
117                definition: Some(format!(
118                    "Resource: {} ({})",
119                    resource.name(),
120                    resource.unit()
121                )),
122            });
123        }
124
125        for pattern in graph.all_patterns() {
126            model.vocabulary.push(SbvrTerm {
127                id: pattern.id().to_string(),
128                name: pattern.name().to_string(),
129                term_type: TermType::IndividualConcept,
130                definition: Some(format!(
131                    "Pattern '{}' matches {}",
132                    pattern.name(),
133                    pattern.regex()
134                )),
135            });
136        }
137
138        model.vocabulary.push(SbvrTerm {
139            id: "verb:transfers".to_string(),
140            name: "transfers".to_string(),
141            term_type: TermType::VerbConcept,
142            definition: Some("Transfer of resource between entities".to_string()),
143        });
144
145        for relation in graph.all_relations() {
146            if relation_predicates.insert(relation.predicate().to_string()) {
147                model.vocabulary.push(SbvrTerm {
148                    id: format!("verb:{}", relation.predicate()),
149                    name: relation.predicate().to_string(),
150                    term_type: TermType::VerbConcept,
151                    definition: Some(format!(
152                        "Fact type predicate '{}' connecting declared roles",
153                        relation.predicate()
154                    )),
155                });
156            }
157
158            model.facts.push(SbvrFactType {
159                id: relation.id().to_string(),
160                subject: graph
161                    .get_role(relation.subject_role())
162                    .map(|role| role.name().to_string())
163                    .unwrap_or_else(|| relation.subject_role().to_string()),
164                verb: relation.predicate().to_string(),
165                object: graph
166                    .get_role(relation.object_role())
167                    .map(|role| role.name().to_string())
168                    .unwrap_or_else(|| relation.object_role().to_string()),
169                destination: relation.via_flow().map(|id| {
170                    graph
171                        .get_resource(id)
172                        .map(|resource| resource.name().to_string())
173                        .unwrap_or_else(|| id.to_string())
174                }),
175                schema_version: SbvrFactType::default_schema_version(),
176            });
177        }
178
179        for flow in graph.all_flows() {
180            model.facts.push(SbvrFactType {
181                id: flow.id().to_string(),
182                subject: flow.from_id().to_string(),
183                verb: "transfers".to_string(),
184                object: flow.resource_id().to_string(),
185                destination: Some(flow.to_id().to_string()),
186                schema_version: SbvrFactType::default_schema_version(),
187            });
188        }
189
190        Ok(model)
191    }
192
193    pub fn to_xmi(&self) -> Result<String, SbvrError> {
194        let mut xmi = String::new();
195
196        xmi.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
197        xmi.push_str("<xmi:XMI xmlns:xmi=\"http://www.omg.org/XMI\" xmlns:sbvr=\"http://www.omg.org/spec/SBVR/20080801\">\n");
198        xmi.push_str("  <sbvr:Vocabulary name=\"SEA_Model\">\n");
199
200        for term in &self.vocabulary {
201            match term.term_type {
202                TermType::GeneralConcept => {
203                    xmi.push_str(&format!(
204                        "    <sbvr:GeneralConcept id=\"{}\" name=\"{}\">\n",
205                        Self::escape_xml(&term.id),
206                        Self::escape_xml(&term.name)
207                    ));
208                    if let Some(def) = &term.definition {
209                        xmi.push_str(&format!(
210                            "      <sbvr:Definition>{}</sbvr:Definition>\n",
211                            Self::escape_xml(def)
212                        ));
213                    }
214                    xmi.push_str("    </sbvr:GeneralConcept>\n");
215                }
216                TermType::IndividualConcept => {
217                    xmi.push_str(&format!(
218                        "    <sbvr:IndividualConcept id=\"{}\" name=\"{}\">\n",
219                        Self::escape_xml(&term.id),
220                        Self::escape_xml(&term.name)
221                    ));
222                    if let Some(def) = &term.definition {
223                        xmi.push_str(&format!(
224                            "      <sbvr:Definition>{}</sbvr:Definition>\n",
225                            Self::escape_xml(def)
226                        ));
227                    }
228                    xmi.push_str("    </sbvr:IndividualConcept>\n");
229                }
230                TermType::VerbConcept => {
231                    xmi.push_str(&format!(
232                        "    <sbvr:VerbConcept id=\"{}\" name=\"{}\">\n",
233                        Self::escape_xml(&term.id),
234                        Self::escape_xml(&term.name)
235                    ));
236                    if let Some(def) = &term.definition {
237                        xmi.push_str(&format!(
238                            "      <sbvr:Definition>{}</sbvr:Definition>\n",
239                            Self::escape_xml(def)
240                        ));
241                    }
242                    xmi.push_str("    </sbvr:VerbConcept>\n");
243                }
244            }
245        }
246
247        for fact in &self.facts {
248            xmi.push_str(&format!(
249                "    <sbvr:FactType id=\"{}\">\n",
250                Self::escape_xml(&fact.id)
251            ));
252            xmi.push_str(&format!(
253                "      <sbvr:SchemaVersion>{}</sbvr:SchemaVersion>\n",
254                Self::escape_xml(&fact.schema_version)
255            ));
256            xmi.push_str(&format!(
257                "      <sbvr:Subject>{}</sbvr:Subject>\n",
258                Self::escape_xml(&fact.subject)
259            ));
260            xmi.push_str(&format!(
261                "      <sbvr:Verb>{}</sbvr:Verb>\n",
262                Self::escape_xml(&fact.verb)
263            ));
264            xmi.push_str(&format!(
265                "      <sbvr:Object>{}</sbvr:Object>\n",
266                Self::escape_xml(&fact.object)
267            ));
268            if let Some(dest) = &fact.destination {
269                xmi.push_str(&format!(
270                    "      <sbvr:Destination>{}</sbvr:Destination>\n",
271                    Self::escape_xml(dest)
272                ));
273            }
274            xmi.push_str("    </sbvr:FactType>\n");
275        }
276
277        for rule in &self.rules {
278            let rule_element = match rule.rule_type {
279                RuleType::Obligation => "Obligation",
280                RuleType::Prohibition => "Prohibition",
281                RuleType::Permission => "Permission",
282                RuleType::Derivation => "Derivation",
283            };
284
285            xmi.push_str(&format!(
286                "    <sbvr:{} id=\"{}\" name=\"{}\">\n",
287                rule_element,
288                Self::escape_xml(&rule.id),
289                Self::escape_xml(&rule.name)
290            ));
291            xmi.push_str(&format!(
292                "      <sbvr:Expression>{}</sbvr:Expression>\n",
293                Self::escape_xml(&rule.expression)
294            ));
295            xmi.push_str(&format!(
296                "      <sbvr:Severity>{}</sbvr:Severity>\n",
297                Self::escape_xml(&rule.severity)
298            ));
299            if let Some(p) = rule.priority {
300                xmi.push_str(&format!("      <sbvr:Priority>{}</sbvr:Priority>\n", p));
301            }
302            xmi.push_str(&format!("    </sbvr:{}>\n", rule_element));
303        }
304
305        xmi.push_str("  </sbvr:Vocabulary>\n");
306        xmi.push_str("</xmi:XMI>\n");
307
308        Ok(xmi)
309    }
310
311    fn escape_xml(s: &str) -> String {
312        s.replace('&', "&amp;")
313            .replace('<', "&lt;")
314            .replace('>', "&gt;")
315            .replace('"', "&quot;")
316            .replace('\'', "&apos;")
317    }
318
319    /// Parse an SBVR XMI document and return a SbvrModel
320    pub fn from_xmi(xmi: &str) -> Result<Self, SbvrError> {
321        let doc = roxmltree::Document::parse(xmi)
322            .map_err(|e| SbvrError::SerializationError(format!("Failed to parse XMI: {}", e)))?;
323
324        let mut model = SbvrModel::new();
325
326        // Find vocabulary node
327        for node in doc.descendants() {
328            if node.has_tag_name("GeneralConcept") {
329                let id = node.attribute("id").unwrap_or_default().to_string();
330                let name = node.attribute("name").unwrap_or_default().to_string();
331                let mut definition = None;
332                for child in node.children() {
333                    if child.has_tag_name("Definition") {
334                        definition = Some(child.text().unwrap_or_default().to_string());
335                    }
336                }
337                model.vocabulary.push(SbvrTerm {
338                    id,
339                    name,
340                    term_type: TermType::GeneralConcept,
341                    definition,
342                });
343            }
344
345            if node.has_tag_name("IndividualConcept") {
346                let id = node.attribute("id").unwrap_or_default().to_string();
347                let name = node.attribute("name").unwrap_or_default().to_string();
348                let mut definition = None;
349                for child in node.children() {
350                    if child.has_tag_name("Definition") {
351                        definition = Some(child.text().unwrap_or_default().to_string());
352                    }
353                }
354                model.vocabulary.push(SbvrTerm {
355                    id,
356                    name,
357                    term_type: TermType::IndividualConcept,
358                    definition,
359                });
360            }
361
362            if node.has_tag_name("VerbConcept") {
363                let id = node.attribute("id").unwrap_or_default().to_string();
364                let name = node.attribute("name").unwrap_or_default().to_string();
365                let mut definition = None;
366                for child in node.children() {
367                    if child.has_tag_name("Definition") {
368                        definition = Some(child.text().unwrap_or_default().to_string());
369                    }
370                }
371                model.vocabulary.push(SbvrTerm {
372                    id,
373                    name,
374                    term_type: TermType::VerbConcept,
375                    definition,
376                });
377            }
378
379            if node.has_tag_name("FactType") {
380                let id = node.attribute("id").unwrap_or_default().to_string();
381                let mut subject = String::new();
382                let mut verb = String::new();
383                let mut object = String::new();
384                let mut destination = None;
385                let mut schema_version = SbvrFactType::default_schema_version();
386
387                for child in node.children() {
388                    if child.has_tag_name("SchemaVersion") {
389                        schema_version = child.text().unwrap_or_default().to_string();
390                    }
391                    if child.has_tag_name("Subject") {
392                        subject = child.text().unwrap_or_default().to_string();
393                    }
394                    if child.has_tag_name("Verb") {
395                        verb = child.text().unwrap_or_default().to_string();
396                    }
397                    if child.has_tag_name("Object") {
398                        object = child.text().unwrap_or_default().to_string();
399                    }
400                    if child.has_tag_name("Destination") {
401                        destination = Some(child.text().unwrap_or_default().to_string());
402                    }
403                }
404
405                model.facts.push(SbvrFactType {
406                    id,
407                    subject,
408                    verb,
409                    object,
410                    destination,
411                    schema_version,
412                });
413            }
414
415            // Parse basic rules (Obligation/Prohibition/Permission/Derivation)
416            if node.has_tag_name("Obligation")
417                || node.has_tag_name("Prohibition")
418                || node.has_tag_name("Permission")
419                || node.has_tag_name("Derivation")
420            {
421                let id = node.attribute("id").unwrap_or_default().to_string();
422                let name = node.attribute("name").unwrap_or_default().to_string();
423                let kind = if node.has_tag_name("Obligation") {
424                    RuleType::Obligation
425                } else if node.has_tag_name("Prohibition") {
426                    RuleType::Prohibition
427                } else if node.has_tag_name("Permission") {
428                    RuleType::Permission
429                } else {
430                    RuleType::Derivation
431                };
432                let mut expression = String::new();
433                let mut severity = String::from("Info");
434                let mut parsed_priority: Option<u8> = None;
435                for child in node.children() {
436                    if child.has_tag_name("Expression") {
437                        expression = child.text().unwrap_or_default().to_string();
438                    }
439                    if child.has_tag_name("Severity") {
440                        severity = child.text().unwrap_or_default().to_string();
441                    }
442                    if child.has_tag_name("Priority") {
443                        if let Some(text) = child.text() {
444                            if let Ok(value) = text.trim().parse::<u8>() {
445                                parsed_priority = Some(value);
446                            }
447                        }
448                    }
449                }
450                let mut rule = SbvrBusinessRule {
451                    id,
452                    name,
453                    rule_type: kind,
454                    expression,
455                    severity,
456                    priority: parsed_priority,
457                };
458                if rule.priority.is_none() {
459                    rule.priority = Some(match rule.rule_type {
460                        RuleType::Obligation => 5,
461                        RuleType::Prohibition => 5,
462                        RuleType::Permission => 1,
463                        RuleType::Derivation => 3,
464                    });
465                }
466                model.rules.push(rule);
467            }
468        }
469
470        Ok(model)
471    }
472
473    /// Convert parsed SbvrModel into a Graph
474    pub fn to_graph(&self) -> Result<crate::graph::Graph, SbvrError> {
475        use crate::graph::Graph;
476        use crate::primitives::{Entity, Flow, Resource};
477        use crate::units::unit_from_string;
478        use rust_decimal::Decimal;
479
480        let mut graph = Graph::new();
481
482        // Create vocabulary terms first
483        for term in &self.vocabulary {
484            match term.term_type {
485                TermType::GeneralConcept => {
486                    let entity =
487                        Entity::new_with_namespace(term.name.clone(), "default".to_string());
488                    graph
489                        .add_entity(entity)
490                        .map_err(SbvrError::SerializationError)?;
491                }
492                TermType::IndividualConcept => {
493                    let unit_symbol = term.definition.as_deref().and_then(|def| {
494                        if let Some(open) = def.rfind('(') {
495                            if let Some(close_offset) = def[open..].find(')') {
496                                let close = open + close_offset;
497                                let candidate = def[open + 1..close].trim();
498                                if !candidate.is_empty() {
499                                    return Some(candidate.to_string());
500                                }
501                            }
502                        }
503                        None
504                    });
505                    let unit_symbol = unit_symbol.unwrap_or_else(|| {
506                        // SBVR definitions currently omit explicit unit metadata, so default to "units".
507                        // Extend the SBVR model if richer unit information becomes available.
508                        "units".to_string()
509                    });
510                    let res = Resource::new_with_namespace(
511                        term.name.clone(),
512                        unit_from_string(&unit_symbol),
513                        "default".to_string(),
514                    );
515                    graph
516                        .add_resource(res)
517                        .map_err(SbvrError::SerializationError)?;
518                }
519                TermType::VerbConcept => {
520                    // We don't represent verbs directly as primitives
521                }
522            }
523        }
524
525        // Now create flows
526        for fact in &self.facts {
527            // find subject (entity)
528            let subject_name = self
529                .vocabulary
530                .iter()
531                .find(|t| t.id == fact.subject)
532                .map(|t| t.name.clone())
533                .unwrap_or(fact.subject.clone());
534
535            let object_name = self
536                .vocabulary
537                .iter()
538                .find(|t| t.id == fact.object)
539                .map(|t| t.name.clone())
540                .unwrap_or(fact.object.clone());
541
542            let destination_name = fact
543                .destination
544                .clone()
545                .and_then(|d| {
546                    self.vocabulary
547                        .iter()
548                        .find(|t| t.id == d)
549                        .map(|t| t.name.clone())
550                })
551                .unwrap_or_default();
552
553            let subject_id = graph.find_entity_by_name(&subject_name).ok_or_else(|| {
554                SbvrError::UnsupportedConstruct(format!("Unknown subject entity: {}", subject_name))
555            })?;
556
557            let destination_id = graph
558                .find_entity_by_name(&destination_name)
559                .ok_or_else(|| {
560                    SbvrError::UnsupportedConstruct(format!(
561                        "Unknown destination entity: {}",
562                        destination_name
563                    ))
564                })?;
565
566            let resource_id = graph.find_resource_by_name(&object_name).ok_or_else(|| {
567                SbvrError::UnsupportedConstruct(format!("Unknown resource: {}", object_name))
568            })?;
569
570            // SBVR FactType does not expose an explicit quantity, so default flows to 1.
571            // This is intentional until the SBVR model is extended with quantity metadata.
572            let quantity = Decimal::from(1);
573
574            let flow = Flow::new(resource_id, subject_id, destination_id, quantity);
575            graph
576                .add_flow(flow)
577                .map_err(SbvrError::SerializationError)?;
578        }
579
580        // Map SBVR Business Rules into Graph Policies
581        for rule in &self.rules {
582            // Parse expression using SEA DSL expression parser
583            let expr = crate::parser::parse_expression_from_str(rule.expression.as_str()).map_err(
584                |e| {
585                    SbvrError::SerializationError(format!("Failed to parse rule expression: {}", e))
586                },
587            )?;
588
589            let mut policy = crate::policy::Policy::new_with_namespace(
590                rule.name.clone(),
591                "default".to_string(),
592                expr,
593            );
594
595            // Map rule type to modality/kind
596            match rule.rule_type {
597                RuleType::Obligation => {
598                    policy = policy.with_modality(crate::policy::PolicyModality::Obligation);
599                    policy = policy.with_kind(crate::policy::PolicyKind::Constraint);
600                }
601                RuleType::Prohibition => {
602                    policy = policy.with_modality(crate::policy::PolicyModality::Prohibition);
603                    policy = policy.with_kind(crate::policy::PolicyKind::Constraint);
604                }
605                RuleType::Permission => {
606                    policy = policy.with_modality(crate::policy::PolicyModality::Permission);
607                    policy = policy.with_kind(crate::policy::PolicyKind::Constraint);
608                }
609                RuleType::Derivation => {
610                    policy = policy.with_modality(crate::policy::PolicyModality::Permission);
611                    policy = policy.with_kind(crate::policy::PolicyKind::Derivation);
612                }
613            }
614
615            // Priority mapping
616            if let Some(p) = rule.priority {
617                policy = policy.with_priority(p as i32);
618            } else {
619                // Default based on rule type
620                let default_p = match rule.rule_type {
621                    RuleType::Obligation => 5,
622                    RuleType::Prohibition => 5,
623                    RuleType::Permission => 1,
624                    RuleType::Derivation => 3,
625                };
626                policy = policy.with_priority(default_p);
627            }
628
629            graph
630                .add_policy(policy)
631                .map_err(SbvrError::SerializationError)?;
632        }
633
634        Ok(graph)
635    }
636}
637
638impl Default for SbvrModel {
639    fn default() -> Self {
640        Self::new()
641    }
642}
643
644impl Graph {
645    pub fn export_sbvr(&self) -> Result<String, SbvrError> {
646        let model = SbvrModel::from_graph(self)?;
647        model.to_xmi()
648    }
649}
650
651#[cfg(test)]
652mod tests {
653    use super::*;
654    use crate::primitives::{Entity, Flow, Resource};
655    use rust_decimal::Decimal;
656
657    #[test]
658    fn test_sbvr_model_creation() {
659        let model = SbvrModel::new();
660        assert_eq!(model.vocabulary.len(), 0);
661        assert_eq!(model.facts.len(), 0);
662        assert_eq!(model.rules.len(), 0);
663    }
664
665    #[test]
666    fn test_export_to_sbvr() {
667        let mut graph = Graph::new();
668
669        let entity1 = Entity::new_with_namespace("Supplier", "supply_chain");
670        let entity2 = Entity::new_with_namespace("Manufacturer", "supply_chain");
671        let resource = Resource::new_with_namespace(
672            "Parts",
673            crate::units::unit_from_string("kg"),
674            "supply_chain",
675        );
676
677        let entity1_id = entity1.id().clone();
678        let entity2_id = entity2.id().clone();
679        let resource_id = resource.id().clone();
680
681        graph.add_entity(entity1).unwrap();
682        graph.add_entity(entity2).unwrap();
683        graph.add_resource(resource).unwrap();
684
685        #[allow(deprecated)]
686        let flow = Flow::new(resource_id, entity1_id, entity2_id, Decimal::new(100, 0));
687        graph.add_flow(flow).unwrap();
688
689        let sbvr_xml = graph.export_sbvr().unwrap();
690
691        assert!(sbvr_xml.contains("<sbvr:FactType"));
692        assert!(sbvr_xml.contains("<sbvr:GeneralConcept"));
693        assert!(sbvr_xml.contains("Supplier"));
694        assert!(sbvr_xml.contains("Manufacturer"));
695    }
696
697    #[test]
698    fn test_xml_escaping() {
699        assert_eq!(SbvrModel::escape_xml("A&B"), "A&amp;B");
700        assert_eq!(SbvrModel::escape_xml("<tag>"), "&lt;tag&gt;");
701        assert_eq!(SbvrModel::escape_xml("\"quote\""), "&quot;quote&quot;");
702    }
703
704    #[test]
705    fn test_sbvr_rule_to_policy() {
706        let mut model = SbvrModel::new();
707
708        // minimal vocabulary
709        model.vocabulary.push(SbvrTerm {
710            id: "e1".to_string(),
711            name: "Warehouse".to_string(),
712            term_type: TermType::GeneralConcept,
713            definition: None,
714        });
715        model.vocabulary.push(SbvrTerm {
716            id: "e2".to_string(),
717            name: "Factory".to_string(),
718            term_type: TermType::GeneralConcept,
719            definition: None,
720        });
721        model.vocabulary.push(SbvrTerm {
722            id: "r1".to_string(),
723            name: "Cameras".to_string(),
724            term_type: TermType::IndividualConcept,
725            definition: None,
726        });
727
728        model.rules.push(SbvrBusinessRule {
729            id: "rule1".to_string(),
730            name: "MustHavePositiveQuantity".to_string(),
731            rule_type: RuleType::Obligation,
732            expression: "forall f in flows: (f.quantity > 0)".to_string(),
733            severity: "Info".to_string(),
734            priority: None,
735        });
736
737        let graph = model.to_graph().expect("SBVR to Graph conversion failed");
738        assert_eq!(graph.policy_count(), 1);
739        let policy = graph.all_policies().into_iter().next().unwrap();
740        assert_eq!(policy.name, "MustHavePositiveQuantity");
741        assert_eq!(policy.priority, 5); // default for Obligation
742        assert_eq!(policy.modality, crate::policy::PolicyModality::Obligation);
743    }
744}