1use super::modelanalytics_type::ModelAnalytics;
8use crate::analytics::{
9 Anomaly, AnomalyType, BenchmarkComparison, BenchmarkLevel, BestPracticeCheck,
10 BestPracticeReport, CheckCategory, ComplexityAssessment, ComplexityLevel, DependencyMetrics,
11 DistributionAnalysis, DistributionStats, DistributionType, Recommendation, RecommendationType,
12 Severity,
13};
14use crate::metamodel::{Aspect, CharacteristicKind, ModelElement};
15use crate::query::ModelQuery;
16use std::collections::{HashMap, HashSet};
17
18impl ModelAnalytics {
19 pub fn analyze(aspect: &Aspect) -> Self {
37 let query = ModelQuery::new(aspect);
38 let complexity_assessment = Self::assess_complexity(aspect, &query);
39 let best_practices = Self::check_best_practices(aspect);
40 let distributions = Self::analyze_distributions(aspect);
41 let dependency_metrics = Self::analyze_dependencies(aspect, &query);
42 let anomalies = Self::detect_anomalies(aspect, &query, &complexity_assessment);
43 let benchmark = Self::benchmark_model(aspect, &complexity_assessment, &distributions);
44 let recommendations = Self::generate_recommendations(
45 aspect,
46 &anomalies,
47 &best_practices,
48 &dependency_metrics,
49 );
50 let quality_score = Self::calculate_quality_score(
51 &complexity_assessment,
52 &best_practices,
53 &dependency_metrics,
54 &anomalies,
55 );
56 Self {
57 quality_score,
58 complexity_assessment,
59 best_practices,
60 distributions,
61 dependency_metrics,
62 anomalies,
63 recommendations,
64 benchmark,
65 }
66 }
67 fn assess_complexity(aspect: &Aspect, query: &ModelQuery) -> ComplexityAssessment {
69 let metrics = query.complexity_metrics();
70 let structural = ((metrics.total_properties as f64 / 50.0) * 100.0).min(100.0);
71 let cognitive = {
72 let property_factor = (metrics.total_properties as f64 / 30.0).min(1.0);
73 let operation_factor = (metrics.total_operations as f64 / 10.0).min(1.0);
74 let depth_factor = (metrics.max_nesting_depth as f64 / 5.0).min(1.0);
75 ((property_factor + operation_factor + depth_factor) / 3.0) * 100.0
76 };
77 let cyclomatic = {
78 let decision_points = metrics.total_operations + metrics.total_entities;
79 (decision_points as f64 * 2.0).min(100.0)
80 };
81 let deps = query.build_dependency_graph();
82 let coupling = if aspect.properties().is_empty() {
83 0.0
84 } else {
85 ((deps.len() as f64 / aspect.properties().len() as f64) * 50.0).min(100.0)
86 };
87 let overall = (structural + cognitive + cyclomatic + coupling) / 4.0;
88 let overall_level = match overall {
89 x if x <= 20.0 => ComplexityLevel::Low,
90 x if x <= 50.0 => ComplexityLevel::Medium,
91 x if x <= 80.0 => ComplexityLevel::High,
92 _ => ComplexityLevel::VeryHigh,
93 };
94 ComplexityAssessment {
95 structural,
96 cognitive,
97 cyclomatic,
98 coupling,
99 overall_level,
100 }
101 }
102 fn check_best_practices(aspect: &Aspect) -> BestPracticeReport {
104 let mut checks = Vec::new();
105 checks.push(BestPracticeCheck {
106 name: "Aspect has preferred name".to_string(),
107 passed: !aspect.metadata.preferred_names.is_empty(),
108 category: CheckCategory::Documentation,
109 details: "Aspect should have at least one preferred name for better readability"
110 .to_string(),
111 });
112 checks.push(BestPracticeCheck {
113 name: "Aspect has description".to_string(),
114 passed: !aspect.metadata.descriptions.is_empty(),
115 category: CheckCategory::Documentation,
116 details: "Aspect should have a description explaining its purpose".to_string(),
117 });
118 let aspect_name = aspect.name();
119 checks.push(BestPracticeCheck {
120 name: "Aspect name follows PascalCase".to_string(),
121 passed: aspect_name.chars().next().is_some_and(|c| c.is_uppercase()),
122 category: CheckCategory::Naming,
123 details: "Aspect names should follow PascalCase convention".to_string(),
124 });
125 let props_with_chars = aspect
126 .properties()
127 .iter()
128 .filter(|p| p.characteristic.is_some())
129 .count();
130 let props_total = aspect.properties().len();
131 checks.push(BestPracticeCheck {
132 name: "All properties have characteristics".to_string(),
133 passed: props_total == 0 || props_with_chars == props_total,
134 category: CheckCategory::Structure,
135 details: format!(
136 "{}/{} properties have characteristics defined",
137 props_with_chars, props_total
138 ),
139 });
140 let valid_property_names = aspect
141 .properties()
142 .iter()
143 .filter(|p| {
144 let name = p.name();
145 !name.is_empty()
146 && name.chars().next().is_some_and(|c| c.is_lowercase())
147 && !name.contains('_')
148 })
149 .count();
150 checks.push(BestPracticeCheck {
151 name: "Properties follow camelCase".to_string(),
152 passed: props_total == 0 || valid_property_names == props_total,
153 category: CheckCategory::Naming,
154 details: format!(
155 "{}/{} properties follow camelCase",
156 valid_property_names, props_total
157 ),
158 });
159 let chars_with_types = aspect
160 .properties()
161 .iter()
162 .filter_map(|p| p.characteristic.as_ref())
163 .filter(|c| c.data_type.is_some())
164 .count();
165 let chars_total = aspect
166 .properties()
167 .iter()
168 .filter(|p| p.characteristic.is_some())
169 .count();
170 checks.push(BestPracticeCheck {
171 name: "Characteristics have data types".to_string(),
172 passed: chars_total == 0 || chars_with_types >= (chars_total * 8 / 10),
173 category: CheckCategory::Types,
174 details: format!(
175 "{}/{} characteristics have data types",
176 chars_with_types, chars_total
177 ),
178 });
179 let has_multi_lang = aspect.metadata.preferred_names.len() > 1
180 || aspect.metadata.descriptions.len() > 1
181 || aspect
182 .properties()
183 .iter()
184 .any(|p| p.metadata.preferred_names.len() > 1);
185 checks.push(BestPracticeCheck {
186 name: "Multi-language support".to_string(),
187 passed: has_multi_lang,
188 category: CheckCategory::Documentation,
189 details: "Model should provide multiple language options for internationalization"
190 .to_string(),
191 });
192 let property_names: Vec<_> = aspect.properties().iter().map(|p| p.name()).collect();
193 let unique_names: HashSet<_> = property_names.iter().collect();
194 checks.push(BestPracticeCheck {
195 name: "No duplicate property names".to_string(),
196 passed: property_names.len() == unique_names.len(),
197 category: CheckCategory::Structure,
198 details: "All property names should be unique".to_string(),
199 });
200 let passed_checks = checks.iter().filter(|c| c.passed).count();
201 let total_checks = checks.len();
202 let compliance_percentage = if total_checks == 0 {
203 100.0
204 } else {
205 (passed_checks as f64 / total_checks as f64) * 100.0
206 };
207 BestPracticeReport {
208 passed_checks,
209 total_checks,
210 compliance_percentage,
211 checks,
212 }
213 }
214 fn analyze_distributions(aspect: &Aspect) -> DistributionAnalysis {
216 let properties = aspect.properties();
217 let property_distribution = if properties.is_empty() {
218 DistributionStats {
219 mean: 0.0,
220 variance: 0.0,
221 std_dev: 0.0,
222 min: 0.0,
223 max: 0.0,
224 }
225 } else {
226 let count = properties.len() as f64;
227 DistributionStats {
228 mean: count,
229 variance: 0.0,
230 std_dev: 0.0,
231 min: count,
232 max: count,
233 }
234 };
235 let mut type_distribution = HashMap::new();
236 for prop in properties {
237 if let Some(char) = &prop.characteristic {
238 if let Some(dtype) = &char.data_type {
239 *type_distribution.entry(dtype.clone()).or_insert(0) += 1;
240 }
241 }
242 }
243 let mut characteristic_distribution = HashMap::new();
244 for prop in properties {
245 if let Some(char) = &prop.characteristic {
246 let kind = match &char.kind {
247 CharacteristicKind::Trait => "Trait",
248 CharacteristicKind::Measurement { .. } => "Measurement",
249 CharacteristicKind::Quantifiable { .. } => "Quantifiable",
250 CharacteristicKind::Enumeration { .. } => "Enumeration",
251 CharacteristicKind::State { .. } => "State",
252 CharacteristicKind::Duration { .. } => "Duration",
253 CharacteristicKind::Collection { .. } => "Collection",
254 CharacteristicKind::List { .. } => "List",
255 CharacteristicKind::Set { .. } => "Set",
256 CharacteristicKind::SortedSet { .. } => "SortedSet",
257 CharacteristicKind::TimeSeries { .. } => "TimeSeries",
258 CharacteristicKind::Code => "Code",
259 CharacteristicKind::Either { .. } => "Either",
260 CharacteristicKind::SingleEntity { .. } => "SingleEntity",
261 CharacteristicKind::StructuredValue { .. } => "StructuredValue",
262 };
263 *characteristic_distribution
264 .entry(kind.to_string())
265 .or_insert(0) += 1;
266 }
267 }
268 let optional_count = properties.iter().filter(|p| p.optional).count();
269 let optionality_ratio = if properties.is_empty() {
270 0.0
271 } else {
272 optional_count as f64 / properties.len() as f64
273 };
274 let collection_count = properties.iter().filter(|p| p.is_collection).count();
275 let collection_percentage = if properties.is_empty() {
276 0.0
277 } else {
278 (collection_count as f64 / properties.len() as f64) * 100.0
279 };
280 DistributionAnalysis {
281 property_distribution,
282 type_distribution,
283 characteristic_distribution,
284 optionality_ratio,
285 collection_percentage,
286 }
287 }
288 fn analyze_dependencies(aspect: &Aspect, query: &ModelQuery) -> DependencyMetrics {
290 let deps = query.build_dependency_graph();
291 let circular = query.detect_circular_dependencies();
292 let total_dependencies = deps.len();
293 let properties = aspect.properties();
294 let avg_dependencies_per_property = if properties.is_empty() {
295 0.0
296 } else {
297 total_dependencies as f64 / properties.len() as f64
298 };
299 let max_dependency_depth = deps.iter().map(|_| 1).max().unwrap_or(0);
300 let possible_deps = if properties.len() <= 1 {
301 1
302 } else {
303 properties.len() * (properties.len() - 1)
304 };
305 let coupling_factor = if possible_deps == 0 {
306 0.0
307 } else {
308 (total_dependencies as f64 / possible_deps as f64).min(1.0)
309 };
310 let cohesion_score = 1.0 - coupling_factor;
311 DependencyMetrics {
312 total_dependencies,
313 avg_dependencies_per_property,
314 max_dependency_depth,
315 coupling_factor,
316 cohesion_score,
317 circular_dependencies: circular.len(),
318 }
319 }
320 fn detect_anomalies(
322 aspect: &Aspect,
323 query: &ModelQuery,
324 complexity: &ComplexityAssessment,
325 ) -> Vec<Anomaly> {
326 let mut anomalies = Vec::new();
327 let properties = aspect.properties();
328 if properties.len() > 50 {
329 anomalies
330 .push(Anomaly {
331 anomaly_type: AnomalyType::HighPropertyCount,
332 severity: Severity::Warning,
333 location: aspect.urn().to_string(),
334 description: format!(
335 "Aspect has {} properties, which is unusually high. Consider splitting into multiple aspects.",
336 properties.len()
337 ),
338 });
339 }
340 if aspect.metadata.preferred_names.is_empty() {
341 anomalies.push(Anomaly {
342 anomaly_type: AnomalyType::MissingDocumentation,
343 severity: Severity::Error,
344 location: aspect.urn().to_string(),
345 description: "Aspect has no preferred name defined".to_string(),
346 });
347 }
348 if aspect.metadata.descriptions.is_empty() {
349 anomalies.push(Anomaly {
350 anomaly_type: AnomalyType::MissingDocumentation,
351 severity: Severity::Warning,
352 location: aspect.urn().to_string(),
353 description: "Aspect has no description defined".to_string(),
354 });
355 }
356 let pascal_case_props = properties
357 .iter()
358 .filter(|p| {
359 let name = p.name();
360 !name.is_empty() && name.chars().next().is_some_and(|c| c.is_uppercase())
361 })
362 .count();
363 if pascal_case_props > 0 && pascal_case_props < properties.len() {
364 anomalies
365 .push(Anomaly {
366 anomaly_type: AnomalyType::InconsistentNaming,
367 severity: Severity::Warning,
368 location: aspect.urn().to_string(),
369 description: format!(
370 "Mixed naming conventions detected: {} properties use PascalCase, {} use camelCase",
371 pascal_case_props, properties.len() - pascal_case_props
372 ),
373 });
374 }
375 if matches!(complexity.overall_level, ComplexityLevel::VeryHigh) {
376 anomalies.push(Anomaly {
377 anomaly_type: AnomalyType::HighCoupling,
378 severity: Severity::Error,
379 location: aspect.urn().to_string(),
380 description: "Model has very high complexity. Refactoring is strongly recommended."
381 .to_string(),
382 });
383 }
384 for prop in properties {
385 if prop.characteristic.is_none() {
386 anomalies.push(Anomaly {
387 anomaly_type: AnomalyType::MissingDocumentation,
388 severity: Severity::Error,
389 location: prop.urn().to_string(),
390 description: "Property has no characteristic defined".to_string(),
391 });
392 }
393 }
394 anomalies
395 }
396 fn generate_recommendations(
398 aspect: &Aspect,
399 anomalies: &[Anomaly],
400 best_practices: &BestPracticeReport,
401 dependencies: &DependencyMetrics,
402 ) -> Vec<Recommendation> {
403 let mut recommendations = Vec::new();
404 if aspect.metadata.preferred_names.is_empty() {
405 recommendations.push(Recommendation {
406 rec_type: RecommendationType::Documentation,
407 severity: Severity::Error,
408 target: aspect.urn().to_string(),
409 message: "Add preferred name to Aspect".to_string(),
410 suggested_action: format!(
411 "Add: aspect.metadata.add_preferred_name(\"en\", \"{}\")",
412 aspect.name()
413 ),
414 });
415 }
416 for check in &best_practices.checks {
417 if !check.passed && check.category == CheckCategory::Naming {
418 recommendations.push(Recommendation {
419 rec_type: RecommendationType::Naming,
420 severity: Severity::Warning,
421 target: aspect.urn().to_string(),
422 message: check.name.clone(),
423 suggested_action:
424 "Review naming conventions and apply consistent camelCase for properties"
425 .to_string(),
426 });
427 }
428 }
429 if dependencies.coupling_factor > 0.5 {
430 recommendations.push(Recommendation {
431 rec_type: RecommendationType::ComplexityReduction,
432 severity: Severity::Warning,
433 target: aspect.urn().to_string(),
434 message: format!(
435 "High coupling detected (factor: {:.2})",
436 dependencies.coupling_factor
437 ),
438 suggested_action:
439 "Consider splitting aspect into smaller, more cohesive components".to_string(),
440 });
441 }
442 if dependencies.circular_dependencies > 0 {
443 recommendations
444 .push(Recommendation {
445 rec_type: RecommendationType::Refactoring,
446 severity: Severity::Error,
447 target: aspect.urn().to_string(),
448 message: format!(
449 "{} circular dependencies detected", dependencies
450 .circular_dependencies
451 ),
452 suggested_action: "Refactor to remove circular dependencies using dependency injection or interfaces"
453 .to_string(),
454 });
455 }
456 if best_practices.compliance_percentage < 80.0 {
457 recommendations.push(Recommendation {
458 rec_type: RecommendationType::BestPractice,
459 severity: Severity::Warning,
460 target: aspect.urn().to_string(),
461 message: format!(
462 "Best practice compliance is low ({:.1}%)",
463 best_practices.compliance_percentage
464 ),
465 suggested_action: "Review failed checks and address them to improve model quality"
466 .to_string(),
467 });
468 }
469 recommendations
470 }
471 fn benchmark_model(
473 aspect: &Aspect,
474 complexity: &ComplexityAssessment,
475 distributions: &DistributionAnalysis,
476 ) -> BenchmarkComparison {
477 const AVG_PROPERTIES: f64 = 15.0;
478 const AVG_COMPLEXITY: f64 = 35.0;
479 const AVG_DOC_COMPLETENESS: f64 = 70.0;
480 let property_count = aspect.properties().len() as f64;
481 let property_count_percentile = (property_count / AVG_PROPERTIES * 50.0).min(100.0);
482 let avg_complexity = (complexity.structural
483 + complexity.cognitive
484 + complexity.cyclomatic
485 + complexity.coupling)
486 / 4.0;
487 let complexity_percentile = (avg_complexity / AVG_COMPLEXITY * 50.0).min(100.0);
488 let has_name = if aspect.metadata.preferred_names.is_empty() {
489 0.0
490 } else {
491 1.0
492 };
493 let has_desc = if aspect.metadata.descriptions.is_empty() {
494 0.0
495 } else {
496 1.0
497 };
498 let doc_completeness = ((has_name + has_desc) / 2.0) * 100.0;
499 let documentation_percentile = (doc_completeness / AVG_DOC_COMPLETENESS * 50.0).min(100.0);
500 let avg_percentile =
501 (property_count_percentile + complexity_percentile + documentation_percentile) / 3.0;
502 let comparison = match avg_percentile {
503 x if x < 40.0 => BenchmarkLevel::BelowAverage,
504 x if x < 60.0 => BenchmarkLevel::Average,
505 x if x < 80.0 => BenchmarkLevel::AboveAverage,
506 _ => BenchmarkLevel::Excellent,
507 };
508 BenchmarkComparison {
509 comparison,
510 property_count_percentile,
511 complexity_percentile,
512 documentation_percentile,
513 }
514 }
515 fn calculate_quality_score(
517 complexity: &ComplexityAssessment,
518 best_practices: &BestPracticeReport,
519 dependencies: &DependencyMetrics,
520 anomalies: &[Anomaly],
521 ) -> f64 {
522 let mut score: f64 = 100.0;
523 let avg_complexity = (complexity.structural
524 + complexity.cognitive
525 + complexity.cyclomatic
526 + complexity.coupling)
527 / 4.0;
528 score -= avg_complexity / 100.0 * 30.0;
529 score -= (100.0 - best_practices.compliance_percentage) / 100.0 * 30.0;
530 score -= dependencies.coupling_factor * 20.0;
531 let critical_count = anomalies
532 .iter()
533 .filter(|a| a.severity == Severity::Critical)
534 .count();
535 let error_count = anomalies
536 .iter()
537 .filter(|a| a.severity == Severity::Error)
538 .count();
539 let warning_count = anomalies
540 .iter()
541 .filter(|a| a.severity == Severity::Warning)
542 .count();
543 score -= (critical_count as f64 * 10.0).min(20.0);
544 score -= (error_count as f64 * 5.0).min(15.0);
545 score -= (warning_count as f64 * 1.0).min(10.0);
546 score.clamp(0.0, 100.0)
547 }
548}