Skip to main content

sbom_tools/tui/
app_impl_constructors.rs

1//! Constructor methods for App.
2
3use super::app::{App, AppMode, AppOverlays, DataContext, ModeStates, TabKind};
4use super::app_states::{
5    MatrixState, MultiDiffState, NavigationContext, SourceDiffState, TimelineState,
6};
7use crate::diff::{DiffResult, MatrixResult, MultiDiffResult, TimelineResult};
8use crate::model::NormalizedSbom;
9use crate::quality::{ComplianceChecker, ComplianceLevel, QualityScorer};
10
11impl App {
12    /// Shared default initialization for all mode-independent fields.
13    /// Mode-specific fields (mode, `active_tab`, and data fields) must be set by the caller.
14    fn base(mode: AppMode) -> Self {
15        Self {
16            mode,
17            active_tab: crate::config::TuiPreferences::load()
18                .last_tab
19                .as_deref()
20                .and_then(TabKind::from_str_opt)
21                .unwrap_or(TabKind::Summary),
22            data: DataContext {
23                diff_result: None,
24                old_sbom: None,
25                new_sbom: None,
26                sbom: None,
27                multi_diff_result: None,
28                timeline_result: None,
29                matrix_result: None,
30                old_sbom_index: None,
31                new_sbom_index: None,
32                sbom_index: None,
33                old_quality: None,
34                new_quality: None,
35                quality_report: None,
36                old_cra_compliance: None,
37                new_cra_compliance: None,
38                old_compliance_results: None,
39                new_compliance_results: None,
40                old_cra_sidecar: None,
41                new_cra_sidecar: None,
42                matching_threshold: 0.85,
43                #[cfg(feature = "enrichment")]
44                enrichment_stats_old: None,
45                #[cfg(feature = "enrichment")]
46                enrichment_stats_new: None,
47            },
48            tabs: ModeStates {
49                multi_diff: MultiDiffState::new(),
50                timeline: TimelineState::new(),
51                matrix: MatrixState::new(),
52            },
53            overlays: AppOverlays::new(),
54            should_quit: false,
55            status_message: None,
56            status_sticky: false,
57            tick: 0,
58            last_export_path: None,
59            navigation_ctx: NavigationContext::new(),
60            security_cache: crate::tui::security::SecurityAnalysisCache::new(),
61            compliance_state: crate::tui::app_states::PolicyComplianceState::new(),
62            export_template: None,
63            tab_window: crate::tui::shared::TabWindow::default(),
64            last_frame_area: None,
65            summary_view: crate::tui::view_states::SummaryView::new(),
66            components_view: crate::tui::view_states::ComponentsView::new(),
67            dependencies_view: crate::tui::view_states::DependenciesView::new(),
68            licenses_view: crate::tui::view_states::LicensesView::new(),
69            vulnerabilities_view: crate::tui::view_states::VulnerabilitiesView::new(),
70            quality_view: crate::tui::view_states::QualityView::new(),
71            compliance_view: crate::tui::view_states::ComplianceView::new(),
72            sidebyside_view: crate::tui::view_states::SideBySideView::new(),
73            graph_changes_view: crate::tui::view_states::GraphChangesView::new(),
74            source_view: crate::tui::view_states::SourceView::new(),
75        }
76    }
77
78    /// Create a new app for diff mode
79    #[must_use]
80    pub fn new_diff(
81        mut diff_result: DiffResult,
82        old_sbom: NormalizedSbom,
83        new_sbom: NormalizedSbom,
84        old_raw: &str,
85        new_raw: &str,
86    ) -> Self {
87        // Calculate quality reports for both SBOMs using profile-aware scoring
88        let old_profile =
89            crate::tui::scoring_profile_for(crate::model::BomProfile::detect(&old_sbom));
90        let old_scorer = QualityScorer::new(old_profile);
91        let old_quality = Some(old_scorer.score(&old_sbom));
92
93        let new_profile =
94            crate::tui::scoring_profile_for(crate::model::BomProfile::detect(&new_sbom));
95        let new_scorer = QualityScorer::new(new_profile);
96        let new_quality = Some(new_scorer.score(&new_sbom));
97
98        // Compute only CRA Phase2 for the summary card; full compliance is lazy
99        let old_cra_compliance =
100            Some(ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&old_sbom));
101        let new_cra_compliance =
102            Some(ComplianceChecker::new(ComplianceLevel::CraPhase2).check(&new_sbom));
103
104        // Build indexes for fast lookups (O(1) instead of O(n))
105        let old_sbom_index = Some(old_sbom.build_index());
106        let new_sbom_index = Some(new_sbom.build_index());
107
108        // Backfill the engine's QualityDelta for TUI-only construction paths
109        // (only the pipeline sets it today); pipeline-provided values win.
110        if diff_result.quality_delta.is_none()
111            && let (Some(oldq), Some(newq)) = (old_quality.as_ref(), new_quality.as_ref())
112        {
113            diff_result.quality_delta = Some(crate::diff::QualityDelta::from_reports(oldq, newq));
114        }
115
116        let mut app = Self::base(AppMode::Diff);
117        let mut source = SourceDiffState::new(old_raw, new_raw);
118        source.populate_annotations(&diff_result);
119        app.source_view = crate::tui::view_states::SourceView::with_state(source);
120        app.data.diff_result = Some(diff_result);
121        app.data.old_sbom = Some(old_sbom);
122        app.data.new_sbom = Some(new_sbom);
123        app.data.old_quality = old_quality;
124        app.data.new_quality = new_quality;
125        app.data.old_cra_compliance = old_cra_compliance;
126        app.data.new_cra_compliance = new_cra_compliance;
127        app.data.old_sbom_index = old_sbom_index;
128        app.data.new_sbom_index = new_sbom_index;
129        app
130    }
131
132    /// Attach per-SBOM CRA sidecar metadata so the compliance tab renders
133    /// sidecar-driven verdicts (EU AI Act high-risk escalation, OSS-Steward,
134    /// EUCC, Article 14) identically to the CLI reports. Each side of the diff
135    /// is judged against its own sidecar. Invalidates any cached compliance
136    /// results so the sidecars take effect on the next
137    /// `ensure_compliance_results`, and recomputes the summary-card CRA
138    /// Phase 2 results (pre-computed bare in `new_diff`) so the summary tab
139    /// agrees with the compliance tab.
140    #[must_use]
141    pub fn with_cra_sidecars(
142        mut self,
143        old_sidecar: Option<crate::model::CraSidecarMetadata>,
144        new_sidecar: Option<crate::model::CraSidecarMetadata>,
145    ) -> Self {
146        self.data.old_compliance_results = None;
147        self.data.new_compliance_results = None;
148        if let (Some(sidecar), Some(old_sbom)) = (&old_sidecar, &self.data.old_sbom) {
149            self.data.old_cra_compliance = Some(
150                ComplianceChecker::new(ComplianceLevel::CraPhase2)
151                    .with_sidecar(sidecar.clone())
152                    .check(old_sbom),
153            );
154        }
155        if let (Some(sidecar), Some(new_sbom)) = (&new_sidecar, &self.data.new_sbom) {
156            self.data.new_cra_compliance = Some(
157                ComplianceChecker::new(ComplianceLevel::CraPhase2)
158                    .with_sidecar(sidecar.clone())
159                    .check(new_sbom),
160            );
161        }
162        self.data.old_cra_sidecar = old_sidecar;
163        self.data.new_cra_sidecar = new_sidecar;
164        self
165    }
166
167    /// Set enrichment statistics for the diff mode
168    #[must_use]
169    #[cfg(feature = "enrichment")]
170    pub fn with_enrichment_stats(
171        mut self,
172        old_stats: Option<crate::enrichment::EnrichmentStats>,
173        new_stats: Option<crate::enrichment::EnrichmentStats>,
174    ) -> Self {
175        self.data.enrichment_stats_old = old_stats;
176        self.data.enrichment_stats_new = new_stats;
177        self
178    }
179
180    /// Get combined enrichment stats for display
181    #[cfg(feature = "enrichment")]
182    #[must_use]
183    pub fn combined_enrichment_stats(&self) -> Option<crate::enrichment::EnrichmentStats> {
184        match (
185            &self.data.enrichment_stats_old,
186            &self.data.enrichment_stats_new,
187        ) {
188            (Some(old), Some(new)) => {
189                let mut combined = old.clone();
190                combined.merge(new);
191                Some(combined)
192            }
193            (Some(stats), None) | (None, Some(stats)) => Some(stats.clone()),
194            (None, None) => None,
195        }
196    }
197
198    /// Create a new app for multi-diff mode
199    #[must_use]
200    pub fn new_multi_diff(result: MultiDiffResult) -> Self {
201        let target_count = result.comparisons.len();
202        // Selection bounds must be set at construction or j/k in the variable
203        // drill-down stays frozen at index 0 (the guard checks total > 0).
204        let variable_count = result.summary.variable_components.len();
205
206        let mut app = Self::base(AppMode::MultiDiff);
207        app.data.multi_diff_result = Some(result);
208        app.tabs.multi_diff = MultiDiffState::new_with_targets(target_count);
209        app.tabs.multi_diff.total_variable_components = variable_count;
210        app
211    }
212
213    /// Create a new app for timeline mode
214    #[must_use]
215    pub fn new_timeline(result: TimelineResult) -> Self {
216        let version_count = result.sboms.len();
217        // Component filter defaults to All, so the unfiltered count is
218        // correct at construction (the 'f' handler resyncs on filter change).
219        // Must be the DISPLAY list's length: filtered_evolution_entries
220        // dedupes components the engine lists as both added and removed, so
221        // the raw list lengths would let the cursor walk past the end.
222        let component_count = crate::tui::views::filtered_evolution_entries(
223            &result,
224            crate::tui::app::TimelineComponentFilter::All,
225        )
226        .len();
227
228        let mut app = Self::base(AppMode::Timeline);
229        app.data.timeline_result = Some(result);
230        app.tabs.timeline = TimelineState::new_with_versions(version_count);
231        app.tabs.timeline.total_components = component_count;
232        app
233    }
234
235    /// Create a new app for matrix mode
236    #[must_use]
237    pub fn new_matrix(result: MatrixResult) -> Self {
238        let sbom_count = result.sboms.len();
239        let cluster_count = result.clustering.as_ref().map_or(0, |c| c.clusters.len());
240
241        let mut app = Self::base(AppMode::Matrix);
242        app.data.matrix_result = Some(result);
243        app.tabs.matrix = MatrixState::new_with_size(sbom_count);
244        app.tabs.matrix.total_clusters = cluster_count;
245        app
246    }
247}