1use crate::SysmlGraph;
2use nomograph_core::traits::KnowledgeGraph;
3use nomograph_core::types::CheckType;
4
5pub struct BadgeData {
6 pub label: String,
7 pub value: String,
8 pub color: String,
9 pub completeness: f64,
10 pub elements: usize,
11 pub relationships: usize,
12 pub findings: usize,
13}
14
15pub fn compute_badge_data(graph: &SysmlGraph) -> BadgeData {
16 let elements = graph.element_count();
17 let relationships = graph.relationship_count();
18
19 let all_checks = vec![
20 CheckType::OrphanRequirements,
21 CheckType::UnverifiedRequirements,
22 CheckType::MissingVerification,
23 CheckType::UnconnectedPorts,
24 CheckType::DanglingReferences,
25 ];
26
27 let mut total_findings = 0;
28 let mut orphan_count = 0;
29 let mut unverified_count = 0;
30
31 for ct in &all_checks {
32 let findings = graph.check(ct.clone());
33 let count = findings.len();
34 total_findings += count;
35 match ct {
36 CheckType::OrphanRequirements => orphan_count = count,
37 CheckType::UnverifiedRequirements => unverified_count = count,
38 _ => {}
39 }
40 }
41
42 let total_requirements = graph
43 .elements()
44 .iter()
45 .filter(|e| e.kind.to_lowercase().contains("requirement"))
46 .count();
47
48 let completeness = if total_requirements > 0 {
49 let gap = (orphan_count + unverified_count).min(total_requirements);
50 1.0 - (gap as f64 / total_requirements as f64)
51 } else {
52 1.0
53 };
54
55 let pct = (completeness * 100.0).round() as u32;
56 let value = format!("{pct}% ({elements}E / {relationships}R)");
57
58 let color = if total_findings == 0 {
59 "#4c1".to_string()
60 } else if completeness >= 0.8 {
61 "#dfb317".to_string()
62 } else if completeness >= 0.5 {
63 "#fe7d37".to_string()
64 } else {
65 "#e05d44".to_string()
66 };
67
68 BadgeData {
69 label: "model health".to_string(),
70 value,
71 color,
72 completeness,
73 elements,
74 relationships,
75 findings: total_findings,
76 }
77}
78
79pub fn render_svg(data: &BadgeData) -> String {
80 let label = &data.label;
81 let value = &data.value;
82 let color = &data.color;
83
84 let label_width = label.len() as u32 * 7 + 12;
85 let value_width = value.len() as u32 * 7 + 12;
86 let total_width = label_width + value_width;
87
88 let label_x = label_width as f32 / 2.0;
89 let value_x = label_width as f32 + value_width as f32 / 2.0;
90
91 format!(
92 r##"<svg xmlns="http://www.w3.org/2000/svg" width="{total_width}" height="20" role="img" aria-label="{label}: {value}">
93 <title>{label}: {value}</title>
94 <linearGradient id="s" x2="0" y2="100%">
95 <stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
96 <stop offset="1" stop-opacity=".1"/>
97 </linearGradient>
98 <clipPath id="r"><rect width="{total_width}" height="20" rx="3" fill="#fff"/></clipPath>
99 <g clip-path="url(#r)">
100 <rect width="{label_width}" height="20" fill="#555"/>
101 <rect x="{label_width}" width="{value_width}" height="20" fill="{color}"/>
102 <rect width="{total_width}" height="20" fill="url(#s)"/>
103 </g>
104 <g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
105 <text x="{label_x}" y="15" fill="#010101" fill-opacity=".3">{label}</text>
106 <text x="{label_x}" y="14" fill="#fff">{label}</text>
107 <text x="{value_x}" y="15" fill="#010101" fill-opacity=".3">{value}</text>
108 <text x="{value_x}" y="14" fill="#fff">{value}</text>
109 </g>
110</svg>"##
111 )
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
119 fn test_render_svg_contains_label_and_value() {
120 let data = BadgeData {
121 label: "model health".to_string(),
122 value: "85% (100E / 200R)".to_string(),
123 color: "#dfb317".to_string(),
124 completeness: 0.85,
125 elements: 100,
126 relationships: 200,
127 findings: 3,
128 };
129 let svg = render_svg(&data);
130 assert!(svg.contains("<svg"));
131 assert!(svg.contains("model health"));
132 assert!(svg.contains("85% (100E / 200R)"));
133 assert!(svg.contains("#dfb317"));
134 }
135
136 #[test]
137 fn test_color_thresholds() {
138 let green = BadgeData {
139 label: "x".into(),
140 value: "x".into(),
141 color: String::new(),
142 completeness: 1.0,
143 elements: 0,
144 relationships: 0,
145 findings: 0,
146 };
147 assert_eq!(compute_color(1.0, 0), "#4c1");
148 assert_eq!(compute_color(0.9, 2), "#dfb317");
149 assert_eq!(compute_color(0.6, 5), "#fe7d37");
150 assert_eq!(compute_color(0.3, 10), "#e05d44");
151 let _ = green;
152 }
153
154 fn compute_color(completeness: f64, findings: usize) -> &'static str {
155 if findings == 0 {
156 "#4c1"
157 } else if completeness >= 0.8 {
158 "#dfb317"
159 } else if completeness >= 0.5 {
160 "#fe7d37"
161 } else {
162 "#e05d44"
163 }
164 }
165
166 #[test]
167 fn test_svg_is_valid_xml() {
168 let data = BadgeData {
169 label: "model health".to_string(),
170 value: "100% (50E / 80R)".to_string(),
171 color: "#4c1".to_string(),
172 completeness: 1.0,
173 elements: 50,
174 relationships: 80,
175 findings: 0,
176 };
177 let svg = render_svg(&data);
178 assert!(svg.starts_with("<svg"));
179 assert!(svg.ends_with("</svg>"));
180 assert!(svg.contains("</g>"));
181 }
182
183 #[test]
184 fn test_badge_data_from_eve_model() {
185 use nomograph_core::traits::KnowledgeGraph;
186 let results = crate::graph::tests::parse_all_eve();
187 let mut graph = crate::SysmlGraph::new();
188 graph.index(results).unwrap();
189 let data = compute_badge_data(&graph);
190 assert!(data.elements > 0);
191 assert!(data.relationships > 0);
192 assert!(data.completeness >= 0.0);
193 assert!(data.completeness <= 1.0);
194 assert!(!data.value.is_empty());
195
196 let svg = render_svg(&data);
197 assert!(svg.contains("model health"));
198 assert!(svg.contains(&format!("{}E", data.elements)));
199 }
200}