Skip to main content

oxirs_samm/analytics/
modelanalytics_analyze_group.rs

1//! # ModelAnalytics - analyze_group Methods
2//!
3//! This module contains method implementations for `ModelAnalytics`.
4//!
5//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
6
7use 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    /// Perform comprehensive analysis on an Aspect model
20    ///
21    /// # Arguments
22    ///
23    /// * `aspect` - The aspect to analyze
24    ///
25    /// # Examples
26    ///
27    /// ```rust
28    /// use oxirs_samm::analytics::ModelAnalytics;
29    /// use oxirs_samm::metamodel::Aspect;
30    ///
31    /// # fn example(aspect: &Aspect) {
32    /// let analytics = ModelAnalytics::analyze(aspect);
33    /// println!("Quality Score: {}/100", analytics.quality_score);
34    /// # }
35    /// ```
36    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    /// Assess complexity across multiple dimensions
68    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    /// Check best practice compliance
103    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    /// Analyze statistical distributions
215    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    /// Analyze dependencies and coupling
289    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    /// Detect anomalies in the model
321    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    /// Generate actionable recommendations
397    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    /// Benchmark model against industry standards
472    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    /// Calculate overall quality score
516    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}