Skip to main content

sbom_tools/diff/
result.rs

1//! Diff result structures.
2
3use crate::model::{CanonicalId, Component, ComponentRef, DependencyEdge, VulnerabilityRef};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Map a severity string to a numeric rank for comparison.
8///
9/// Higher values indicate more severe vulnerabilities.
10/// Returns 0 for unrecognized severity strings.
11fn severity_rank(s: &str) -> u8 {
12    match s.to_lowercase().as_str() {
13        "critical" => 4,
14        "high" => 3,
15        "medium" => 2,
16        "low" => 1,
17        _ => 0,
18    }
19}
20
21/// Complete result of an SBOM diff operation.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23#[must_use]
24pub struct DiffResult {
25    /// Summary statistics
26    pub summary: DiffSummary,
27    /// Component changes
28    pub components: ChangeSet<ComponentChange>,
29    /// Dependency changes
30    pub dependencies: ChangeSet<DependencyChange>,
31    /// License changes
32    pub licenses: LicenseChanges,
33    /// Vulnerability changes
34    pub vulnerabilities: VulnerabilityChanges,
35    /// Total semantic score
36    pub semantic_score: f64,
37    /// Document-level metadata changes (author, tool, timestamp, spec version,
38    /// lifecycle phase, signature, document/primary-component version)
39    #[serde(default, skip_serializing_if = "Vec::is_empty")]
40    pub metadata_changes: Vec<MetadataChange>,
41    /// Graph structural changes (only populated if graph diffing is enabled)
42    #[serde(default)]
43    pub graph_changes: Vec<DependencyGraphChange>,
44    /// Summary of graph changes
45    #[serde(default)]
46    pub graph_summary: Option<GraphChangeSummary>,
47    /// Number of custom matching rules applied
48    #[serde(default)]
49    pub rules_applied: usize,
50    /// Quality impact of this diff (computed post-diff)
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub quality_delta: Option<QualityDelta>,
53    /// Matching quality metrics (populated during diff)
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub match_metrics: Option<MatchMetrics>,
56}
57
58impl DiffResult {
59    /// Create a new empty diff result
60    pub fn new() -> Self {
61        Self {
62            summary: DiffSummary::default(),
63            components: ChangeSet::new(),
64            dependencies: ChangeSet::new(),
65            licenses: LicenseChanges::default(),
66            vulnerabilities: VulnerabilityChanges::default(),
67            semantic_score: 0.0,
68            metadata_changes: Vec::new(),
69            graph_changes: Vec::new(),
70            graph_summary: None,
71            rules_applied: 0,
72            quality_delta: None,
73            match_metrics: None,
74        }
75    }
76
77    /// Calculate and update summary statistics
78    pub fn calculate_summary(&mut self) {
79        self.summary.components_added = self.components.added.len();
80        self.summary.components_removed = self.components.removed.len();
81        self.summary.components_modified = self.components.modified.len();
82
83        self.summary.dependencies_added = self.dependencies.added.len();
84        self.summary.dependencies_removed = self.dependencies.removed.len();
85        self.summary.graph_changes_count = self.graph_changes.len();
86        self.summary.metadata_changes_count = self.metadata_changes.len();
87
88        self.summary.total_changes = self.summary.components_added
89            + self.summary.components_removed
90            + self.summary.components_modified
91            + self.summary.dependencies_added
92            + self.summary.dependencies_removed
93            + self.summary.graph_changes_count
94            + self.summary.metadata_changes_count;
95
96        self.summary.vulnerabilities_introduced = self.vulnerabilities.introduced.len();
97        self.summary.vulnerabilities_resolved = self.vulnerabilities.resolved.len();
98        self.summary.vulnerabilities_persistent = self.vulnerabilities.persistent.len();
99
100        self.summary.licenses_added = self.licenses.new_licenses.len();
101        self.summary.licenses_removed = self.licenses.removed_licenses.len();
102    }
103
104    /// Check if there are any changes.
105    ///
106    /// Checks both the pre-computed summary and the source-of-truth fields to be
107    /// safe regardless of whether `calculate_summary()` was called.
108    #[must_use]
109    pub fn has_changes(&self) -> bool {
110        self.summary.total_changes > 0
111            || !self.components.is_empty()
112            || !self.dependencies.is_empty()
113            || !self.graph_changes.is_empty()
114            || !self.metadata_changes.is_empty()
115            || !self.vulnerabilities.introduced.is_empty()
116            || !self.vulnerabilities.resolved.is_empty()
117    }
118
119    /// Find a component change by canonical ID
120    #[must_use]
121    pub fn find_component_by_id(&self, id: &CanonicalId) -> Option<&ComponentChange> {
122        let id_str = id.value();
123        self.components
124            .added
125            .iter()
126            .chain(self.components.removed.iter())
127            .chain(self.components.modified.iter())
128            .find(|c| c.id == id_str)
129    }
130
131    /// Find a component change by ID string
132    #[must_use]
133    pub fn find_component_by_id_str(&self, id_str: &str) -> Option<&ComponentChange> {
134        self.components
135            .added
136            .iter()
137            .chain(self.components.removed.iter())
138            .chain(self.components.modified.iter())
139            .find(|c| c.id == id_str)
140    }
141
142    /// Get all component changes as a flat list with their indices for navigation
143    #[must_use]
144    pub fn all_component_changes(&self) -> Vec<&ComponentChange> {
145        self.components
146            .added
147            .iter()
148            .chain(self.components.removed.iter())
149            .chain(self.components.modified.iter())
150            .collect()
151    }
152
153    /// Find vulnerabilities affecting a specific component by ID
154    #[must_use]
155    pub fn find_vulns_for_component(
156        &self,
157        component_id: &CanonicalId,
158    ) -> Vec<&VulnerabilityDetail> {
159        let id_str = component_id.value();
160        self.vulnerabilities
161            .introduced
162            .iter()
163            .chain(self.vulnerabilities.resolved.iter())
164            .chain(self.vulnerabilities.persistent.iter())
165            .filter(|v| v.component_id == id_str)
166            .collect()
167    }
168
169    /// Build an index of component IDs to their changes for fast lookup
170    #[must_use]
171    pub fn build_component_id_index(&self) -> HashMap<String, &ComponentChange> {
172        self.components
173            .added
174            .iter()
175            .chain(&self.components.removed)
176            .chain(&self.components.modified)
177            .map(|c| (c.id.clone(), c))
178            .collect()
179    }
180
181    /// Filter vulnerabilities by minimum severity level
182    pub fn filter_by_severity(&mut self, min_severity: &str) {
183        let min_sev = severity_rank(min_severity);
184
185        self.vulnerabilities
186            .introduced
187            .retain(|v| severity_rank(&v.severity) >= min_sev);
188        self.vulnerabilities
189            .resolved
190            .retain(|v| severity_rank(&v.severity) >= min_sev);
191        self.vulnerabilities
192            .persistent
193            .retain(|v| severity_rank(&v.severity) >= min_sev);
194
195        // Recalculate summary
196        self.calculate_summary();
197    }
198
199    /// Filter out vulnerabilities where VEX status is `NotAffected` or `Fixed`.
200    ///
201    /// Keeps vulnerabilities that are `Affected`, `UnderInvestigation`, or have no VEX status.
202    pub fn filter_by_vex(&mut self) {
203        self.vulnerabilities
204            .introduced
205            .retain(VulnerabilityDetail::is_vex_actionable);
206        self.vulnerabilities
207            .resolved
208            .retain(VulnerabilityDetail::is_vex_actionable);
209        self.vulnerabilities
210            .persistent
211            .retain(VulnerabilityDetail::is_vex_actionable);
212
213        self.calculate_summary();
214    }
215}
216
217impl Default for DiffResult {
218    fn default() -> Self {
219        Self::new()
220    }
221}
222
223/// Quality and compliance impact of the diff.
224///
225/// Computed by comparing quality scores of old vs new SBOMs.
226/// Enables tracking whether a change improves or degrades SBOM quality.
227#[derive(Debug, Clone, Default, Serialize, Deserialize)]
228pub struct QualityDelta {
229    /// Overall score change (positive = improvement)
230    pub overall_score_delta: f32,
231    /// Old grade
232    #[serde(default, skip_serializing_if = "Option::is_none")]
233    pub old_grade: Option<crate::quality::QualityGrade>,
234    /// New grade
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub new_grade: Option<crate::quality::QualityGrade>,
237    /// Per-category score deltas
238    pub category_deltas: Vec<CategoryDelta>,
239    /// Categories that regressed (score decreased by >1 point)
240    pub regressions: Vec<String>,
241    /// Categories that improved (score increased by >1 point)
242    pub improvements: Vec<String>,
243    /// Compliance violation count change (positive = more violations)
244    pub violation_count_delta: i32,
245}
246
247/// Score delta for a specific quality category.
248#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
249pub struct CategoryDelta {
250    /// Category name (e.g., "Completeness", "Identifiers")
251    pub category: String,
252    /// Score in old SBOM
253    pub old_score: f32,
254    /// Score in new SBOM
255    pub new_score: f32,
256    /// Change (new - old)
257    pub delta: f32,
258}
259
260impl QualityDelta {
261    /// Compute quality delta by comparing two quality reports.
262    #[must_use]
263    pub fn from_reports(
264        old: &crate::quality::QualityReport,
265        new: &crate::quality::QualityReport,
266    ) -> Self {
267        let categories = [
268            (
269                "Completeness",
270                old.completeness_score,
271                new.completeness_score,
272            ),
273            ("Identifiers", old.identifier_score, new.identifier_score),
274            ("Licenses", old.license_score, new.license_score),
275            ("Dependencies", old.dependency_score, new.dependency_score),
276            ("Integrity", old.integrity_score, new.integrity_score),
277            ("Provenance", old.provenance_score, new.provenance_score),
278        ];
279
280        let mut category_deltas: Vec<CategoryDelta> = categories
281            .iter()
282            .map(|(name, old_s, new_s)| CategoryDelta {
283                category: (*name).to_string(),
284                old_score: *old_s,
285                new_score: *new_s,
286                delta: new_s - old_s,
287            })
288            .collect();
289
290        // Handle optional categories (VulnDocs and Lifecycle)
291        if let (Some(old_v), Some(new_v)) = (old.vulnerability_score, new.vulnerability_score) {
292            category_deltas.push(CategoryDelta {
293                category: "VulnDocs".to_string(),
294                old_score: old_v,
295                new_score: new_v,
296                delta: new_v - old_v,
297            });
298        }
299        if let (Some(old_l), Some(new_l)) = (old.lifecycle_score, new.lifecycle_score) {
300            category_deltas.push(CategoryDelta {
301                category: "Lifecycle".to_string(),
302                old_score: old_l,
303                new_score: new_l,
304                delta: new_l - old_l,
305            });
306        }
307
308        let regressions: Vec<String> = category_deltas
309            .iter()
310            .filter(|d| d.delta < -1.0)
311            .map(|d| d.category.clone())
312            .collect();
313
314        let improvements: Vec<String> = category_deltas
315            .iter()
316            .filter(|d| d.delta > 1.0)
317            .map(|d| d.category.clone())
318            .collect();
319
320        // Compute compliance violation delta
321        let old_violations = old.compliance.error_count + old.compliance.warning_count;
322        let new_violations = new.compliance.error_count + new.compliance.warning_count;
323
324        Self {
325            overall_score_delta: new.overall_score - old.overall_score,
326            old_grade: Some(old.grade),
327            new_grade: Some(new.grade),
328            category_deltas,
329            regressions,
330            improvements,
331            violation_count_delta: new_violations as i32 - old_violations as i32,
332        }
333    }
334}
335
336/// Metrics about the component matching process.
337///
338/// Provides visibility into matching quality for debugging and tuning.
339#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
340pub struct MatchMetrics {
341    /// Number of exact matches (PURL, CPE, or canonical ID)
342    pub exact_matches: usize,
343    /// Number of fuzzy matches (below exact threshold)
344    pub fuzzy_matches: usize,
345    /// Number of custom rule matches
346    pub rule_matches: usize,
347    /// Components in old SBOM with no match
348    pub unmatched_old: usize,
349    /// Components in new SBOM with no match
350    pub unmatched_new: usize,
351    /// Average match confidence score
352    pub avg_match_score: f64,
353    /// Minimum match confidence score
354    pub min_match_score: f64,
355}
356
357/// Summary statistics for the diff
358#[derive(Debug, Clone, Default, Serialize, Deserialize)]
359pub struct DiffSummary {
360    pub total_changes: usize,
361    pub components_added: usize,
362    pub components_removed: usize,
363    pub components_modified: usize,
364    pub dependencies_added: usize,
365    pub dependencies_removed: usize,
366    pub graph_changes_count: usize,
367    pub metadata_changes_count: usize,
368    pub vulnerabilities_introduced: usize,
369    pub vulnerabilities_resolved: usize,
370    pub vulnerabilities_persistent: usize,
371    pub licenses_added: usize,
372    pub licenses_removed: usize,
373}
374
375/// Generic change set for added/removed/modified items
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub struct ChangeSet<T> {
378    pub added: Vec<T>,
379    pub removed: Vec<T>,
380    pub modified: Vec<T>,
381}
382
383impl<T> ChangeSet<T> {
384    #[must_use]
385    pub const fn new() -> Self {
386        Self {
387            added: Vec::new(),
388            removed: Vec::new(),
389            modified: Vec::new(),
390        }
391    }
392
393    #[must_use]
394    pub fn is_empty(&self) -> bool {
395        self.added.is_empty() && self.removed.is_empty() && self.modified.is_empty()
396    }
397
398    #[must_use]
399    pub fn total(&self) -> usize {
400        self.added.len() + self.removed.len() + self.modified.len()
401    }
402}
403
404impl<T> Default for ChangeSet<T> {
405    fn default() -> Self {
406        Self::new()
407    }
408}
409
410/// Information about how a component was matched.
411///
412/// Included in JSON output to explain why components were correlated.
413#[derive(Debug, Clone, Serialize, Deserialize)]
414pub struct MatchInfo {
415    /// Match confidence score (0.0 - 1.0)
416    pub score: f64,
417    /// Matching method used (`ExactIdentifier`, Alias, Fuzzy, etc.)
418    pub method: String,
419    /// Human-readable explanation
420    pub reason: String,
421    /// Detailed score breakdown (optional)
422    #[serde(skip_serializing_if = "Vec::is_empty")]
423    pub score_breakdown: Vec<MatchScoreComponent>,
424    /// Normalizations applied during matching
425    #[serde(skip_serializing_if = "Vec::is_empty")]
426    pub normalizations: Vec<String>,
427    /// Confidence interval for the match score
428    #[serde(skip_serializing_if = "Option::is_none")]
429    pub confidence_interval: Option<ConfidenceInterval>,
430}
431
432/// Confidence interval for match score.
433///
434/// Provides uncertainty bounds around the match score, useful for
435/// understanding match reliability.
436#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct ConfidenceInterval {
438    /// Lower bound of confidence (0.0 - 1.0)
439    pub lower: f64,
440    /// Upper bound of confidence (0.0 - 1.0)
441    pub upper: f64,
442    /// Confidence level (e.g., 0.95 for 95% CI)
443    pub level: f64,
444}
445
446impl ConfidenceInterval {
447    /// Create a new confidence interval.
448    #[must_use]
449    pub const fn new(lower: f64, upper: f64, level: f64) -> Self {
450        Self {
451            lower: lower.clamp(0.0, 1.0),
452            upper: upper.clamp(0.0, 1.0),
453            level,
454        }
455    }
456
457    /// Create a 95% confidence interval from a score and standard error.
458    ///
459    /// Uses ±1.96 × SE for 95% CI.
460    #[must_use]
461    pub fn from_score_and_error(score: f64, std_error: f64) -> Self {
462        let margin = 1.96 * std_error;
463        Self::new(score - margin, score + margin, 0.95)
464    }
465
466    /// Create a simple confidence interval based on the matching tier.
467    ///
468    /// Exact matches have tight intervals, fuzzy matches have wider intervals.
469    #[must_use]
470    pub fn from_tier(score: f64, tier: &str) -> Self {
471        let margin = match tier {
472            "ExactIdentifier" => 0.0,
473            "Alias" => 0.02,
474            "EcosystemRule" => 0.03,
475            "CustomRule" => 0.05,
476            "Fuzzy" => 0.08,
477            _ => 0.10,
478        };
479        Self::new(score - margin, score + margin, 0.95)
480    }
481
482    /// Get the width of the interval.
483    #[must_use]
484    pub fn width(&self) -> f64 {
485        self.upper - self.lower
486    }
487}
488
489/// A component of the match score for JSON output.
490#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct MatchScoreComponent {
492    /// Name of this score component
493    pub name: String,
494    /// Weight applied
495    pub weight: f64,
496    /// Raw score
497    pub raw_score: f64,
498    /// Weighted contribution
499    pub weighted_score: f64,
500    /// Description
501    pub description: String,
502}
503
504/// Component change information
505#[derive(Debug, Clone, Serialize, Deserialize)]
506pub struct ComponentChange {
507    /// Component canonical ID (string for serialization)
508    pub id: String,
509    /// Typed canonical ID for navigation (skipped in JSON output for backward compat)
510    #[serde(skip)]
511    pub canonical_id: Option<CanonicalId>,
512    /// Component reference with ID and name together
513    #[serde(skip)]
514    pub component_ref: Option<ComponentRef>,
515    /// Old component ID (for modified components)
516    #[serde(skip)]
517    pub old_canonical_id: Option<CanonicalId>,
518    /// Component name
519    pub name: String,
520    /// Old version (if existed)
521    pub old_version: Option<String>,
522    /// New version (if exists)
523    pub new_version: Option<String>,
524    /// Ecosystem
525    pub ecosystem: Option<String>,
526    /// Change type
527    pub change_type: ChangeType,
528    /// Detailed field changes
529    pub field_changes: Vec<FieldChange>,
530    /// Associated cost
531    pub cost: u32,
532    /// Match information (for modified components, explains how old/new were correlated)
533    #[serde(skip_serializing_if = "Option::is_none")]
534    pub match_info: Option<MatchInfo>,
535}
536
537impl ComponentChange {
538    /// Create a new component addition
539    pub fn added(component: &Component, cost: u32) -> Self {
540        Self {
541            id: component.canonical_id.to_string(),
542            canonical_id: Some(component.canonical_id.clone()),
543            component_ref: Some(ComponentRef::from_component(component)),
544            old_canonical_id: None,
545            name: component.name.clone(),
546            old_version: None,
547            new_version: component.version.clone(),
548            ecosystem: component
549                .ecosystem
550                .as_ref()
551                .map(std::string::ToString::to_string),
552            change_type: ChangeType::Added,
553            field_changes: Vec::new(),
554            cost,
555            match_info: None,
556        }
557    }
558
559    /// Create a new component removal
560    pub fn removed(component: &Component, cost: u32) -> Self {
561        Self {
562            id: component.canonical_id.to_string(),
563            canonical_id: Some(component.canonical_id.clone()),
564            component_ref: Some(ComponentRef::from_component(component)),
565            old_canonical_id: Some(component.canonical_id.clone()),
566            name: component.name.clone(),
567            old_version: component.version.clone(),
568            new_version: None,
569            ecosystem: component
570                .ecosystem
571                .as_ref()
572                .map(std::string::ToString::to_string),
573            change_type: ChangeType::Removed,
574            field_changes: Vec::new(),
575            cost,
576            match_info: None,
577        }
578    }
579
580    /// Create a component modification
581    pub fn modified(
582        old: &Component,
583        new: &Component,
584        field_changes: Vec<FieldChange>,
585        cost: u32,
586    ) -> Self {
587        Self {
588            id: new.canonical_id.to_string(),
589            canonical_id: Some(new.canonical_id.clone()),
590            component_ref: Some(ComponentRef::from_component(new)),
591            old_canonical_id: Some(old.canonical_id.clone()),
592            name: new.name.clone(),
593            old_version: old.version.clone(),
594            new_version: new.version.clone(),
595            ecosystem: new.ecosystem.as_ref().map(std::string::ToString::to_string),
596            change_type: ChangeType::Modified,
597            field_changes,
598            cost,
599            match_info: None,
600        }
601    }
602
603    /// Create a component modification with match explanation
604    pub fn modified_with_match(
605        old: &Component,
606        new: &Component,
607        field_changes: Vec<FieldChange>,
608        cost: u32,
609        match_info: MatchInfo,
610    ) -> Self {
611        Self {
612            id: new.canonical_id.to_string(),
613            canonical_id: Some(new.canonical_id.clone()),
614            component_ref: Some(ComponentRef::from_component(new)),
615            old_canonical_id: Some(old.canonical_id.clone()),
616            name: new.name.clone(),
617            old_version: old.version.clone(),
618            new_version: new.version.clone(),
619            ecosystem: new.ecosystem.as_ref().map(std::string::ToString::to_string),
620            change_type: ChangeType::Modified,
621            field_changes,
622            cost,
623            match_info: Some(match_info),
624        }
625    }
626
627    /// Add match information to an existing change
628    #[must_use]
629    pub fn with_match_info(mut self, match_info: MatchInfo) -> Self {
630        self.match_info = Some(match_info);
631        self
632    }
633
634    /// Get the typed canonical ID, falling back to parsing from string if needed
635    #[must_use]
636    pub fn get_canonical_id(&self) -> CanonicalId {
637        self.canonical_id.clone().unwrap_or_else(|| {
638            CanonicalId::from_name_version(
639                &self.name,
640                self.new_version.as_deref().or(self.old_version.as_deref()),
641            )
642        })
643    }
644
645    /// Get a `ComponentRef` for this change
646    #[must_use]
647    pub fn get_component_ref(&self) -> ComponentRef {
648        self.component_ref.clone().unwrap_or_else(|| {
649            ComponentRef::with_version(
650                self.get_canonical_id(),
651                &self.name,
652                self.new_version
653                    .clone()
654                    .or_else(|| self.old_version.clone()),
655            )
656        })
657    }
658}
659
660impl MatchInfo {
661    /// Create from a `MatchExplanation`
662    #[must_use]
663    pub fn from_explanation(explanation: &crate::matching::MatchExplanation) -> Self {
664        let method = format!("{:?}", explanation.tier);
665        let ci = ConfidenceInterval::from_tier(explanation.score, &method);
666        Self {
667            score: explanation.score,
668            method,
669            reason: explanation.reason.clone(),
670            score_breakdown: explanation
671                .score_breakdown
672                .iter()
673                .map(|c| MatchScoreComponent {
674                    name: c.name.clone(),
675                    weight: c.weight,
676                    raw_score: c.raw_score,
677                    weighted_score: c.weighted_score,
678                    description: c.description.clone(),
679                })
680                .collect(),
681            normalizations: explanation.normalizations_applied.clone(),
682            confidence_interval: Some(ci),
683        }
684    }
685
686    /// Create a simple match info without detailed breakdown
687    #[must_use]
688    pub fn simple(score: f64, method: &str, reason: &str) -> Self {
689        let ci = ConfidenceInterval::from_tier(score, method);
690        Self {
691            score,
692            method: method.to_string(),
693            reason: reason.to_string(),
694            score_breakdown: Vec::new(),
695            normalizations: Vec::new(),
696            confidence_interval: Some(ci),
697        }
698    }
699
700    /// Create a match info with a custom confidence interval
701    #[must_use]
702    pub const fn with_confidence_interval(mut self, ci: ConfidenceInterval) -> Self {
703        self.confidence_interval = Some(ci);
704        self
705    }
706}
707
708/// Type of change
709#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
710pub enum ChangeType {
711    Added,
712    Removed,
713    Modified,
714    Unchanged,
715}
716
717/// Individual field change
718#[derive(Debug, Clone, Serialize, Deserialize)]
719pub struct FieldChange {
720    pub field: String,
721    pub old_value: Option<String>,
722    pub new_value: Option<String>,
723}
724
725/// Whether a document-metadata field was added, removed, or modified.
726#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
727#[serde(rename_all = "lowercase")]
728pub enum MetadataChangeKind {
729    /// Field gained a value (old absent, new present).
730    Added,
731    /// Field lost a value (old present, new absent).
732    Removed,
733    /// Field's value changed (both present, different).
734    Modified,
735}
736
737impl std::fmt::Display for MetadataChangeKind {
738    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
739        let s = match self {
740            Self::Added => "added",
741            Self::Removed => "removed",
742            Self::Modified => "modified",
743        };
744        f.write_str(s)
745    }
746}
747
748/// A document-level metadata change between two SBOMs.
749///
750/// Surfaces changes that are invisible in the component/dependency/vulnerability
751/// passes: author or tool churn, timestamp updates, spec-version upgrades
752/// (e.g. CycloneDX 1.5 -> 1.7), lifecycle-phase transitions, signature presence
753/// or algorithm changes, and document- or primary-component version bumps. This
754/// is the cross-cutting metadata signal the BSI gap analysis flagged as missing.
755#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
756pub struct MetadataChange {
757    /// Stable field key (e.g. `name`, `spec_version`, `created`, `creator.tool`,
758    /// `lifecycle_phase`, `signature.algorithm`, `primary_component_version`).
759    pub field: String,
760    /// Value in the old SBOM (`None` when the field was absent / added).
761    pub old_value: Option<String>,
762    /// Value in the new SBOM (`None` when the field was removed).
763    pub new_value: Option<String>,
764    /// Whether the field was added, removed, or modified.
765    pub kind: MetadataChangeKind,
766}
767
768impl MetadataChange {
769    /// Build a metadata change from a field key and the two optional values,
770    /// inferring the [`MetadataChangeKind`] from presence. Returns `None` when
771    /// the values are equal (no change to report).
772    #[must_use]
773    pub fn from_values(
774        field: impl Into<String>,
775        old_value: Option<String>,
776        new_value: Option<String>,
777    ) -> Option<Self> {
778        if old_value == new_value {
779            return None;
780        }
781        let kind = match (&old_value, &new_value) {
782            (None, Some(_)) => MetadataChangeKind::Added,
783            (Some(_), None) => MetadataChangeKind::Removed,
784            _ => MetadataChangeKind::Modified,
785        };
786        Some(Self {
787            field: field.into(),
788            old_value,
789            new_value,
790            kind,
791        })
792    }
793}
794
795/// Dependency change information
796#[derive(Debug, Clone, Serialize, Deserialize)]
797pub struct DependencyChange {
798    /// Source component
799    pub from: String,
800    /// Target component
801    pub to: String,
802    /// Relationship type
803    pub relationship: String,
804    /// Dependency scope
805    #[serde(default, skip_serializing_if = "Option::is_none")]
806    pub scope: Option<String>,
807    /// Change type
808    pub change_type: ChangeType,
809}
810
811impl DependencyChange {
812    #[must_use]
813    pub fn added(edge: &DependencyEdge) -> Self {
814        Self {
815            from: edge.from.to_string(),
816            to: edge.to.to_string(),
817            relationship: edge.relationship.to_string(),
818            scope: edge.scope.as_ref().map(std::string::ToString::to_string),
819            change_type: ChangeType::Added,
820        }
821    }
822
823    #[must_use]
824    pub fn removed(edge: &DependencyEdge) -> Self {
825        Self {
826            from: edge.from.to_string(),
827            to: edge.to.to_string(),
828            relationship: edge.relationship.to_string(),
829            scope: edge.scope.as_ref().map(std::string::ToString::to_string),
830            change_type: ChangeType::Removed,
831        }
832    }
833}
834
835/// License change information
836#[derive(Debug, Clone, Default, Serialize, Deserialize)]
837pub struct LicenseChanges {
838    /// Newly introduced licenses
839    pub new_licenses: Vec<LicenseChange>,
840    /// Removed licenses
841    pub removed_licenses: Vec<LicenseChange>,
842    /// License conflicts
843    pub conflicts: Vec<LicenseConflict>,
844    /// Components with license changes
845    pub component_changes: Vec<ComponentLicenseChange>,
846}
847
848/// Individual license change
849#[derive(Debug, Clone, Serialize, Deserialize)]
850pub struct LicenseChange {
851    /// License expression
852    pub license: String,
853    /// Components using this license
854    pub components: Vec<String>,
855    /// License family
856    pub family: String,
857}
858
859/// License conflict information
860#[derive(Debug, Clone, Serialize, Deserialize)]
861pub struct LicenseConflict {
862    pub license_a: String,
863    pub license_b: String,
864    pub component: String,
865    pub description: String,
866}
867
868/// Component-level license change
869#[derive(Debug, Clone, Serialize, Deserialize)]
870pub struct ComponentLicenseChange {
871    pub component_id: String,
872    pub component_name: String,
873    pub old_licenses: Vec<String>,
874    pub new_licenses: Vec<String>,
875}
876
877/// A VEX state change for a vulnerability between old and new SBOMs.
878#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
879pub struct VexStatusChange {
880    /// Vulnerability ID (e.g., "CVE-2023-1234")
881    pub vuln_id: String,
882    /// Affected component name
883    pub component_name: String,
884    /// Old VEX state (None = no VEX in old SBOM)
885    #[serde(default, skip_serializing_if = "Option::is_none")]
886    pub old_state: Option<crate::model::VexState>,
887    /// New VEX state (None = no VEX in new SBOM)
888    #[serde(default, skip_serializing_if = "Option::is_none")]
889    pub new_state: Option<crate::model::VexState>,
890}
891
892/// Vulnerability change information
893#[derive(Debug, Clone, Default, Serialize, Deserialize)]
894pub struct VulnerabilityChanges {
895    /// Newly introduced vulnerabilities
896    pub introduced: Vec<VulnerabilityDetail>,
897    /// Resolved vulnerabilities
898    pub resolved: Vec<VulnerabilityDetail>,
899    /// Persistent vulnerabilities (present in both)
900    pub persistent: Vec<VulnerabilityDetail>,
901    /// VEX state transitions detected across persistent vulnerabilities
902    #[serde(default, skip_serializing_if = "Vec::is_empty")]
903    pub vex_changes: Vec<VexStatusChange>,
904}
905
906impl VulnerabilityChanges {
907    /// Count vulnerabilities by severity
908    #[must_use]
909    pub fn introduced_by_severity(&self) -> HashMap<String, usize> {
910        // Pre-allocate for typical severity levels (critical, high, medium, low, unknown)
911        let mut counts = HashMap::with_capacity(5);
912        for vuln in &self.introduced {
913            *counts.entry(vuln.severity.clone()).or_insert(0) += 1;
914        }
915        counts
916    }
917
918    /// Get critical and high severity introduced vulnerabilities
919    #[must_use]
920    pub fn critical_and_high_introduced(&self) -> Vec<&VulnerabilityDetail> {
921        self.introduced
922            .iter()
923            .filter(|v| v.severity == "Critical" || v.severity == "High")
924            .collect()
925    }
926
927    /// Compute VEX coverage summary across all vulnerability categories.
928    pub fn vex_summary(&self) -> VexCoverageSummary {
929        let all_vulns: Vec<&VulnerabilityDetail> = self
930            .introduced
931            .iter()
932            .chain(&self.resolved)
933            .chain(&self.persistent)
934            .collect();
935
936        let total = all_vulns.len();
937        let mut with_vex = 0;
938        let mut by_state: HashMap<crate::model::VexState, usize> = HashMap::with_capacity(4);
939        let mut actionable = 0;
940
941        for vuln in &all_vulns {
942            if let Some(ref state) = vuln.vex_state {
943                with_vex += 1;
944                *by_state.entry(state.clone()).or_insert(0) += 1;
945            }
946            if vuln.is_vex_actionable() {
947                actionable += 1;
948            }
949        }
950
951        // Vulns without VEX (gaps) — both introduced and persistent are flagged
952        let introduced_without_vex = self
953            .introduced
954            .iter()
955            .filter(|v| v.vex_state.is_none())
956            .count();
957
958        let persistent_without_vex = self
959            .persistent
960            .iter()
961            .filter(|v| v.vex_state.is_none())
962            .count();
963
964        VexCoverageSummary {
965            total_vulns: total,
966            with_vex,
967            without_vex: total - with_vex,
968            actionable,
969            coverage_pct: if total > 0 {
970                (with_vex as f64 / total as f64) * 100.0
971            } else {
972                100.0
973            },
974            by_state,
975            introduced_without_vex,
976            persistent_without_vex,
977        }
978    }
979}
980
981/// VEX coverage summary for vulnerability changes.
982#[derive(Debug, Clone, Serialize, Deserialize)]
983#[must_use]
984pub struct VexCoverageSummary {
985    /// Total vulnerabilities across all categories
986    pub total_vulns: usize,
987    /// Vulnerabilities with a VEX statement
988    pub with_vex: usize,
989    /// Vulnerabilities without a VEX statement
990    pub without_vex: usize,
991    /// Vulnerabilities that are VEX-actionable (no NotAffected/Fixed)
992    pub actionable: usize,
993    /// VEX coverage percentage (0.0-100.0)
994    pub coverage_pct: f64,
995    /// Breakdown by VEX state
996    pub by_state: HashMap<crate::model::VexState, usize>,
997    /// Introduced vulnerabilities without VEX (gaps requiring attention)
998    pub introduced_without_vex: usize,
999    /// Persistent vulnerabilities without VEX (ongoing gaps)
1000    pub persistent_without_vex: usize,
1001}
1002
1003/// SLA status for vulnerability remediation tracking
1004#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1005pub enum SlaStatus {
1006    /// Past SLA deadline by N days
1007    Overdue(i64),
1008    /// Due within 3 days (N days remaining)
1009    DueSoon(i64),
1010    /// Within SLA window (N days remaining)
1011    OnTrack(i64),
1012    /// No SLA deadline applicable
1013    NoDueDate,
1014}
1015
1016impl SlaStatus {
1017    /// Format for display (e.g., "3d late", "2d left", "45d old")
1018    #[must_use]
1019    pub fn display(&self, days_since_published: Option<i64>) -> String {
1020        match self {
1021            Self::Overdue(days) => format!("{days}d late"),
1022            Self::DueSoon(days) | Self::OnTrack(days) => format!("{days}d left"),
1023            Self::NoDueDate => {
1024                days_since_published.map_or_else(|| "-".to_string(), |age| format!("{age}d old"))
1025            }
1026        }
1027    }
1028
1029    /// Check if this is an overdue status
1030    #[must_use]
1031    pub const fn is_overdue(&self) -> bool {
1032        matches!(self, Self::Overdue(_))
1033    }
1034
1035    /// Check if this is due soon (approaching deadline)
1036    #[must_use]
1037    pub const fn is_due_soon(&self) -> bool {
1038        matches!(self, Self::DueSoon(_))
1039    }
1040}
1041
1042/// Detailed vulnerability information
1043#[derive(Debug, Clone, Serialize, Deserialize)]
1044pub struct VulnerabilityDetail {
1045    /// Vulnerability ID
1046    pub id: String,
1047    /// Source database
1048    pub source: String,
1049    /// Severity level
1050    pub severity: String,
1051    /// CVSS score
1052    pub cvss_score: Option<f32>,
1053    /// Affected component ID (string for serialization)
1054    pub component_id: String,
1055    /// Typed canonical ID for the component (skipped in JSON for backward compat)
1056    #[serde(skip)]
1057    pub component_canonical_id: Option<CanonicalId>,
1058    /// Component reference with ID and name together
1059    #[serde(skip)]
1060    pub component_ref: Option<ComponentRef>,
1061    /// Affected component name
1062    pub component_name: String,
1063    /// Affected version
1064    pub version: Option<String>,
1065    /// CWE identifiers
1066    pub cwes: Vec<String>,
1067    /// Description
1068    pub description: Option<String>,
1069    /// Remediation info
1070    pub remediation: Option<String>,
1071    /// Whether this vulnerability is in CISA's Known Exploited Vulnerabilities catalog
1072    #[serde(default)]
1073    pub is_kev: bool,
1074    /// FIRST EPSS exploit-probability score (0.0 - 1.0), if enriched
1075    #[serde(default, skip_serializing_if = "Option::is_none")]
1076    pub epss_score: Option<f64>,
1077    /// Dependency depth (1 = direct, 2+ = transitive, None = unknown)
1078    #[serde(default)]
1079    pub component_depth: Option<u32>,
1080    /// Date vulnerability was published (ISO 8601)
1081    #[serde(default)]
1082    pub published_date: Option<String>,
1083    /// KEV due date (CISA mandated remediation deadline)
1084    #[serde(default)]
1085    pub kev_due_date: Option<String>,
1086    /// Days since published (positive = past)
1087    #[serde(default)]
1088    pub days_since_published: Option<i64>,
1089    /// Days until KEV due date (negative = overdue)
1090    #[serde(default)]
1091    pub days_until_due: Option<i64>,
1092    /// VEX state for this vulnerability's component (if available)
1093    #[serde(default, skip_serializing_if = "Option::is_none")]
1094    pub vex_state: Option<crate::model::VexState>,
1095    /// VEX justification (from per-vuln or component-level VEX)
1096    #[serde(default, skip_serializing_if = "Option::is_none")]
1097    pub vex_justification: Option<crate::model::VexJustification>,
1098    /// VEX impact statement (from per-vuln or component-level VEX)
1099    #[serde(default, skip_serializing_if = "Option::is_none")]
1100    pub vex_impact_statement: Option<String>,
1101}
1102
1103impl VulnerabilityDetail {
1104    /// Whether this vulnerability is VEX-actionable (not resolved by vendor analysis).
1105    ///
1106    /// Returns `true` if the VEX state is `Affected`, `UnderInvestigation`, or absent.
1107    /// Returns `false` if the VEX state is `NotAffected` or `Fixed`.
1108    #[must_use]
1109    pub const fn is_vex_actionable(&self) -> bool {
1110        !matches!(
1111            self.vex_state,
1112            Some(crate::model::VexState::NotAffected | crate::model::VexState::Fixed)
1113        )
1114    }
1115
1116    /// Create from a vulnerability reference and component
1117    pub fn from_ref(vuln: &VulnerabilityRef, component: &Component) -> Self {
1118        // Calculate days since published (published is DateTime<Utc>)
1119        let days_since_published = vuln.published.map(|dt| {
1120            let today = chrono::Utc::now().date_naive();
1121            (today - dt.date_naive()).num_days()
1122        });
1123
1124        // Format published date as string for serialization
1125        let published_date = vuln.published.map(|dt| dt.format("%Y-%m-%d").to_string());
1126
1127        // Get KEV info if present
1128        let (kev_due_date, days_until_due) = vuln.kev_info.as_ref().map_or((None, None), |kev| {
1129            (
1130                Some(kev.due_date.format("%Y-%m-%d").to_string()),
1131                Some(kev.days_until_due()),
1132            )
1133        });
1134
1135        Self {
1136            id: vuln.id.clone(),
1137            source: vuln.source.to_string(),
1138            severity: vuln
1139                .severity
1140                .as_ref()
1141                .map_or_else(|| "Unknown".to_string(), std::string::ToString::to_string),
1142            cvss_score: vuln.max_cvss_score(),
1143            component_id: component.canonical_id.to_string(),
1144            component_canonical_id: Some(component.canonical_id.clone()),
1145            component_ref: Some(ComponentRef::from_component(component)),
1146            component_name: component.name.clone(),
1147            version: component.version.clone(),
1148            cwes: vuln.cwes.clone(),
1149            description: vuln.description.clone(),
1150            remediation: vuln.remediation.as_ref().map(|r| {
1151                format!(
1152                    "{}: {}",
1153                    r.remediation_type,
1154                    r.description.as_deref().unwrap_or("")
1155                )
1156            }),
1157            is_kev: vuln.is_kev,
1158            epss_score: vuln.epss_score,
1159            component_depth: None,
1160            published_date,
1161            kev_due_date,
1162            days_since_published,
1163            days_until_due,
1164            vex_state: {
1165                let vex_source = vuln.vex_status.as_ref().or(component.vex_status.as_ref());
1166                vex_source.map(|v| v.status.clone())
1167            },
1168            vex_justification: {
1169                let vex_source = vuln.vex_status.as_ref().or(component.vex_status.as_ref());
1170                vex_source.and_then(|v| v.justification.clone())
1171            },
1172            vex_impact_statement: {
1173                let vex_source = vuln.vex_status.as_ref().or(component.vex_status.as_ref());
1174                vex_source.and_then(|v| v.impact_statement.clone())
1175            },
1176        }
1177    }
1178
1179    /// Create from a vulnerability reference and component with known depth
1180    #[must_use]
1181    pub fn from_ref_with_depth(
1182        vuln: &VulnerabilityRef,
1183        component: &Component,
1184        depth: Option<u32>,
1185    ) -> Self {
1186        let mut detail = Self::from_ref(vuln, component);
1187        detail.component_depth = depth;
1188        detail
1189    }
1190
1191    /// Calculate SLA status based on KEV due date or severity-based policy
1192    ///
1193    /// Priority order:
1194    /// 1. KEV due date (CISA mandated deadline)
1195    /// 2. Severity-based SLA (Critical=1d, High=7d, Medium=30d, Low=90d)
1196    #[must_use]
1197    pub fn sla_status(&self) -> SlaStatus {
1198        // KEV due date takes priority
1199        if let Some(days) = self.days_until_due {
1200            if days < 0 {
1201                return SlaStatus::Overdue(-days);
1202            } else if days <= 3 {
1203                return SlaStatus::DueSoon(days);
1204            }
1205            return SlaStatus::OnTrack(days);
1206        }
1207
1208        // Fall back to severity-based SLA
1209        if let Some(age_days) = self.days_since_published {
1210            let sla_days = match self.severity.to_lowercase().as_str() {
1211                "critical" => 1,
1212                "high" => 7,
1213                "medium" => 30,
1214                "low" => 90,
1215                _ => return SlaStatus::NoDueDate,
1216            };
1217            let remaining = sla_days - age_days;
1218            if remaining < 0 {
1219                return SlaStatus::Overdue(-remaining);
1220            } else if remaining <= 3 {
1221                return SlaStatus::DueSoon(remaining);
1222            }
1223            return SlaStatus::OnTrack(remaining);
1224        }
1225
1226        SlaStatus::NoDueDate
1227    }
1228
1229    /// Get the typed component canonical ID
1230    #[must_use]
1231    pub fn get_component_id(&self) -> CanonicalId {
1232        self.component_canonical_id.clone().unwrap_or_else(|| {
1233            CanonicalId::from_name_version(&self.component_name, self.version.as_deref())
1234        })
1235    }
1236
1237    /// Get a `ComponentRef` for the affected component
1238    #[must_use]
1239    pub fn get_component_ref(&self) -> ComponentRef {
1240        self.component_ref.clone().unwrap_or_else(|| {
1241            ComponentRef::with_version(
1242                self.get_component_id(),
1243                &self.component_name,
1244                self.version.clone(),
1245            )
1246        })
1247    }
1248}
1249
1250// ============================================================================
1251// Graph-Aware Diffing Types
1252// ============================================================================
1253
1254/// Represents a structural change in the dependency graph
1255#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1256pub struct DependencyGraphChange {
1257    /// The component involved in the change
1258    pub component_id: CanonicalId,
1259    /// Human-readable component name
1260    pub component_name: String,
1261    /// The type of structural change
1262    pub change: DependencyChangeType,
1263    /// Assessed impact of this change
1264    pub impact: GraphChangeImpact,
1265}
1266
1267/// Types of dependency graph structural changes
1268#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1269#[non_exhaustive]
1270pub enum DependencyChangeType {
1271    /// A new dependency link was added
1272    DependencyAdded {
1273        dependency_id: CanonicalId,
1274        dependency_name: String,
1275    },
1276
1277    /// A dependency link was removed
1278    DependencyRemoved {
1279        dependency_id: CanonicalId,
1280        dependency_name: String,
1281    },
1282
1283    /// Dependency relationship or scope changed (same endpoints, different attributes)
1284    RelationshipChanged {
1285        dependency_id: CanonicalId,
1286        dependency_name: String,
1287        old_relationship: String,
1288        new_relationship: String,
1289        old_scope: Option<String>,
1290        new_scope: Option<String>,
1291    },
1292
1293    /// A dependency was reparented (had exactly one parent in both, but different)
1294    Reparented {
1295        dependency_id: CanonicalId,
1296        dependency_name: String,
1297        old_parent_id: CanonicalId,
1298        old_parent_name: String,
1299        new_parent_id: CanonicalId,
1300        new_parent_name: String,
1301    },
1302
1303    /// Dependency depth changed (e.g., transitive became direct)
1304    DepthChanged {
1305        old_depth: u32, // 1 = root, 2 = direct, 3+ = transitive
1306        new_depth: u32,
1307    },
1308}
1309
1310impl DependencyChangeType {
1311    /// Get a short description of the change type
1312    #[must_use]
1313    pub const fn kind(&self) -> &'static str {
1314        match self {
1315            Self::DependencyAdded { .. } => "added",
1316            Self::DependencyRemoved { .. } => "removed",
1317            Self::RelationshipChanged { .. } => "relationship_changed",
1318            Self::Reparented { .. } => "reparented",
1319            Self::DepthChanged { .. } => "depth_changed",
1320        }
1321    }
1322}
1323
1324/// Impact level of a graph change
1325#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
1326pub enum GraphChangeImpact {
1327    /// Internal reorganization, no functional change
1328    Low,
1329    /// Depth or type change, may affect build/runtime
1330    Medium,
1331    /// Security-relevant component relationship changed
1332    High,
1333    /// Vulnerable component promoted to direct dependency
1334    Critical,
1335}
1336
1337impl GraphChangeImpact {
1338    #[must_use]
1339    pub const fn as_str(&self) -> &'static str {
1340        match self {
1341            Self::Low => "low",
1342            Self::Medium => "medium",
1343            Self::High => "high",
1344            Self::Critical => "critical",
1345        }
1346    }
1347
1348    /// Parse from a string label. Returns Low for unrecognized values.
1349    #[must_use]
1350    pub fn from_label(s: &str) -> Self {
1351        match s.to_lowercase().as_str() {
1352            "critical" => Self::Critical,
1353            "high" => Self::High,
1354            "medium" => Self::Medium,
1355            _ => Self::Low,
1356        }
1357    }
1358}
1359
1360impl std::fmt::Display for GraphChangeImpact {
1361    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1362        write!(f, "{}", self.as_str())
1363    }
1364}
1365
1366/// Summary statistics for graph changes
1367#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1368pub struct GraphChangeSummary {
1369    pub total_changes: usize,
1370    pub dependencies_added: usize,
1371    pub dependencies_removed: usize,
1372    pub relationship_changed: usize,
1373    pub reparented: usize,
1374    pub depth_changed: usize,
1375    pub by_impact: GraphChangesByImpact,
1376}
1377
1378impl GraphChangeSummary {
1379    /// Build summary from a list of changes
1380    #[must_use]
1381    pub fn from_changes(changes: &[DependencyGraphChange]) -> Self {
1382        let mut summary = Self {
1383            total_changes: changes.len(),
1384            ..Default::default()
1385        };
1386
1387        for change in changes {
1388            match &change.change {
1389                DependencyChangeType::DependencyAdded { .. } => summary.dependencies_added += 1,
1390                DependencyChangeType::DependencyRemoved { .. } => summary.dependencies_removed += 1,
1391                DependencyChangeType::RelationshipChanged { .. } => {
1392                    summary.relationship_changed += 1;
1393                }
1394                DependencyChangeType::Reparented { .. } => summary.reparented += 1,
1395                DependencyChangeType::DepthChanged { .. } => summary.depth_changed += 1,
1396            }
1397
1398            match change.impact {
1399                GraphChangeImpact::Low => summary.by_impact.low += 1,
1400                GraphChangeImpact::Medium => summary.by_impact.medium += 1,
1401                GraphChangeImpact::High => summary.by_impact.high += 1,
1402                GraphChangeImpact::Critical => summary.by_impact.critical += 1,
1403            }
1404        }
1405
1406        summary
1407    }
1408}
1409
1410#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1411pub struct GraphChangesByImpact {
1412    pub low: usize,
1413    pub medium: usize,
1414    pub high: usize,
1415    pub critical: usize,
1416}