1use std::collections::HashMap;
2
3use crate::element::SysmlElement;
4use crate::graph::SysmlGraph;
5use crate::relationship::SysmlRelationship;
6use nomograph_core::traits::KnowledgeGraph;
7use nomograph_core::types::Finding;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub enum MetamodelCheck {
11 SatisfyTargetMustBeRequirement,
12 VerifyTargetMustBeRequirement,
13 AllocateSourceLogicalTargetPhysical,
14 PortsMustHaveType,
15 BindingConnectorCompatibility,
16}
17
18fn build_element_map(elements: &[SysmlElement]) -> HashMap<String, &SysmlElement> {
19 let mut map = HashMap::new();
20 for elem in elements {
21 map.insert(elem.qualified_name.to_lowercase(), elem);
22 let short = elem
23 .qualified_name
24 .rsplit("::")
25 .next()
26 .unwrap_or(&elem.qualified_name)
27 .to_lowercase();
28 map.entry(short).or_insert(elem);
29 }
30 map
31}
32
33fn resolve_element<'a>(
34 name: &str,
35 elem_map: &HashMap<String, &'a SysmlElement>,
36) -> Option<&'a SysmlElement> {
37 let lower = name.to_lowercase();
38 if let Some(e) = elem_map.get(&lower) {
39 return Some(e);
40 }
41 let short = name.rsplit("::").next().unwrap_or(name).to_lowercase();
42 elem_map.get(&short).copied()
43}
44
45fn is_requirement(elem: &SysmlElement) -> bool {
46 elem.kind.to_lowercase().contains("requirement")
47}
48
49fn is_port(elem: &SysmlElement) -> bool {
50 elem.kind.to_lowercase().contains("port")
51}
52
53fn is_logical_kind(kind: &str) -> bool {
54 let k = kind.to_lowercase();
55 k.contains("requirement")
56 || k.contains("use_case")
57 || k.contains("action")
58 || k.contains("state")
59}
60
61fn is_physical_kind(kind: &str) -> bool {
62 let k = kind.to_lowercase();
63 k.contains("part") || k.contains("item") || k.contains("port")
64}
65
66fn check_satisfy_targets(
67 rels: &[SysmlRelationship],
68 elem_map: &HashMap<String, &SysmlElement>,
69) -> Vec<Finding> {
70 rels.iter()
71 .filter(|r| r.kind.eq_ignore_ascii_case("satisfy"))
72 .filter_map(|r| {
73 let target = resolve_element(&r.target, elem_map);
74 match target {
75 Some(e) if is_requirement(e) => None,
76 Some(e) => Some(Finding {
77 check_type: nomograph_core::types::CheckType::DanglingReferences,
78 element: r.source.clone(),
79 message: format!(
80 "satisfy target '{}' is {} (expected requirement)",
81 r.target, e.kind
82 ),
83 file_path: r.file_path.clone(),
84 span: r.span.clone(),
85 }),
86 None => None,
87 }
88 })
89 .collect()
90}
91
92fn check_verify_targets(
93 rels: &[SysmlRelationship],
94 elem_map: &HashMap<String, &SysmlElement>,
95) -> Vec<Finding> {
96 rels.iter()
97 .filter(|r| r.kind.eq_ignore_ascii_case("verify"))
98 .filter_map(|r| {
99 let target = resolve_element(&r.target, elem_map);
100 match target {
101 Some(e) if is_requirement(e) => None,
102 Some(e) => Some(Finding {
103 check_type: nomograph_core::types::CheckType::DanglingReferences,
104 element: r.source.clone(),
105 message: format!(
106 "verify target '{}' is {} (expected requirement)",
107 r.target, e.kind
108 ),
109 file_path: r.file_path.clone(),
110 span: r.span.clone(),
111 }),
112 None => None,
113 }
114 })
115 .collect()
116}
117
118fn check_allocate_layers(
119 rels: &[SysmlRelationship],
120 elem_map: &HashMap<String, &SysmlElement>,
121) -> Vec<Finding> {
122 rels.iter()
123 .filter(|r| r.kind.eq_ignore_ascii_case("allocate"))
124 .filter_map(|r| {
125 let source = resolve_element(&r.source, elem_map);
126 let target = resolve_element(&r.target, elem_map);
127 match (source, target) {
128 (Some(s), Some(t)) => {
129 let mut findings = Vec::new();
130 if !is_logical_kind(&s.kind) {
131 findings.push(Finding {
132 check_type: nomograph_core::types::CheckType::DanglingReferences,
133 element: r.source.clone(),
134 message: format!(
135 "allocate source '{}' is {} (expected logical element)",
136 r.source, s.kind
137 ),
138 file_path: r.file_path.clone(),
139 span: r.span.clone(),
140 });
141 }
142 if !is_physical_kind(&t.kind) {
143 findings.push(Finding {
144 check_type: nomograph_core::types::CheckType::DanglingReferences,
145 element: r.target.clone(),
146 message: format!(
147 "allocate target '{}' is {} (expected physical element)",
148 r.target, t.kind
149 ),
150 file_path: r.file_path.clone(),
151 span: r.span.clone(),
152 });
153 }
154 if findings.is_empty() {
155 None
156 } else {
157 Some(findings)
158 }
159 }
160 _ => None,
161 }
162 })
163 .flatten()
164 .collect()
165}
166
167fn check_ports_have_type(elements: &[SysmlElement], rels: &[SysmlRelationship]) -> Vec<Finding> {
168 let typed_sources: std::collections::HashSet<String> = rels
169 .iter()
170 .filter(|r| r.kind.eq_ignore_ascii_case("typedby"))
171 .flat_map(|r| {
172 let short = r
173 .source
174 .rsplit("::")
175 .next()
176 .unwrap_or(&r.source)
177 .to_lowercase();
178 vec![r.source.to_lowercase(), short]
179 })
180 .collect();
181
182 elements
183 .iter()
184 .filter(|e| is_port(e))
185 .filter(|e| {
186 let qname = e.qualified_name.to_lowercase();
187 let short = e
188 .qualified_name
189 .rsplit("::")
190 .next()
191 .unwrap_or(&e.qualified_name)
192 .to_lowercase();
193 !typed_sources.contains(&qname) && !typed_sources.contains(&short)
194 })
195 .map(|e| Finding {
196 check_type: nomograph_core::types::CheckType::DanglingReferences,
197 element: e.qualified_name.clone(),
198 message: "port has no TypedBy relationship (missing type definition)".to_string(),
199 file_path: e.file_path.clone(),
200 span: e.span.clone(),
201 })
202 .collect()
203}
204
205fn check_binding_connector_compatibility(
206 rels: &[SysmlRelationship],
207 _elem_map: &HashMap<String, &SysmlElement>,
208) -> Vec<Finding> {
209 let typed_by: HashMap<String, String> = rels
210 .iter()
211 .filter(|r| r.kind.eq_ignore_ascii_case("typedby"))
212 .map(|r| (r.source.to_lowercase(), r.target.to_lowercase()))
213 .collect();
214
215 rels.iter()
216 .filter(|r| {
217 let k = r.kind.to_lowercase();
218 k == "connect" || k == "bind" || k == "binding"
219 })
220 .filter_map(|r| {
221 let src_type = typed_by.get(&r.source.to_lowercase());
222 let tgt_type = typed_by.get(&r.target.to_lowercase());
223
224 match (src_type, tgt_type) {
225 (Some(st), Some(tt)) if st != tt => Some(Finding {
226 check_type: nomograph_core::types::CheckType::DanglingReferences,
227 element: r.source.clone(),
228 message: format!(
229 "binding connector connects incompatible types: '{}' ({}) to '{}' ({})",
230 r.source, st, r.target, tt
231 ),
232 file_path: r.file_path.clone(),
233 span: r.span.clone(),
234 }),
235 _ => None,
236 }
237 })
238 .collect()
239}
240
241pub fn run_metamodel_checks(graph: &SysmlGraph) -> Vec<Finding> {
242 let elem_map = build_element_map(graph.elements());
243 let rels = graph.relationships();
244 let elements = graph.elements();
245
246 let mut findings = Vec::new();
247 findings.extend(check_satisfy_targets(rels, &elem_map));
248 findings.extend(check_verify_targets(rels, &elem_map));
249 findings.extend(check_allocate_layers(rels, &elem_map));
250 findings.extend(check_ports_have_type(elements, rels));
251 findings.extend(check_binding_connector_compatibility(rels, &elem_map));
252 findings
253}
254
255pub fn run_single_metamodel_check(graph: &SysmlGraph, check: &MetamodelCheck) -> Vec<Finding> {
256 let elem_map = build_element_map(graph.elements());
257 let rels = graph.relationships();
258 let elements = graph.elements();
259
260 match check {
261 MetamodelCheck::SatisfyTargetMustBeRequirement => check_satisfy_targets(rels, &elem_map),
262 MetamodelCheck::VerifyTargetMustBeRequirement => check_verify_targets(rels, &elem_map),
263 MetamodelCheck::AllocateSourceLogicalTargetPhysical => {
264 check_allocate_layers(rels, &elem_map)
265 }
266 MetamodelCheck::PortsMustHaveType => check_ports_have_type(elements, rels),
267 MetamodelCheck::BindingConnectorCompatibility => {
268 check_binding_connector_compatibility(rels, &elem_map)
269 }
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use crate::parser::SysmlParser;
277 use nomograph_core::traits::Parser as NomographParser;
278 use std::path::PathBuf;
279
280 fn fixture_dir() -> PathBuf {
281 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/fixtures/eve")
282 }
283
284 fn walkdir(dir: PathBuf) -> Vec<PathBuf> {
285 let mut files = Vec::new();
286 if let Ok(entries) = std::fs::read_dir(&dir) {
287 for entry in entries.flatten() {
288 let path = entry.path();
289 if path.is_dir() {
290 files.extend(walkdir(path));
291 } else {
292 files.push(path);
293 }
294 }
295 }
296 files
297 }
298
299 fn build_eve_graph() -> SysmlGraph {
300 let parser = SysmlParser::new();
301 let mut results = Vec::new();
302 for entry in walkdir(fixture_dir()) {
303 if entry.extension().and_then(|e| e.to_str()) == Some("sysml") {
304 let source = std::fs::read_to_string(&entry).expect("read fixture");
305 let result = parser.parse(&source, &entry).expect("parse fixture");
306 results.push(result);
307 }
308 }
309 let mut graph = SysmlGraph::new();
310 graph.index(results).expect("index");
311 graph
312 }
313
314 #[test]
315 fn test_metamodel_checks_run() {
316 let graph = build_eve_graph();
317 let findings = run_metamodel_checks(&graph);
318 assert!(
319 findings.iter().all(|f| !f.message.is_empty()),
320 "all findings should have messages"
321 );
322 }
323
324 #[test]
325 fn test_ports_have_type_check() {
326 let graph = build_eve_graph();
327 let findings = run_single_metamodel_check(&graph, &MetamodelCheck::PortsMustHaveType);
328 for f in &findings {
329 assert!(
330 f.message.contains("port has no TypedBy"),
331 "finding should be about missing port type"
332 );
333 }
334 }
335
336 #[test]
337 fn test_satisfy_target_check() {
338 let graph = build_eve_graph();
339 let findings =
340 run_single_metamodel_check(&graph, &MetamodelCheck::SatisfyTargetMustBeRequirement);
341 for f in &findings {
342 assert!(
343 f.message.contains("satisfy target"),
344 "finding should be about satisfy target"
345 );
346 }
347 }
348
349 #[test]
350 fn test_verify_target_check() {
351 let graph = build_eve_graph();
352 let findings =
353 run_single_metamodel_check(&graph, &MetamodelCheck::VerifyTargetMustBeRequirement);
354 for f in &findings {
355 assert!(
356 f.message.contains("verify target"),
357 "finding should be about verify target"
358 );
359 }
360 }
361}