Skip to main content

weavatrix_scan/
summary.rs

1use crate::report::{
2    CompactScanReport, ScanCacheStats, ScanReport, ScanTermination, SkipKind, SkippedEntry,
3};
4use std::collections::BTreeMap;
5use std::fmt;
6
7/// Path-free aggregate scan output for logs, telemetry, and higher-level tools.
8///
9/// `recorded_skips` and `skipped_by_kind` describe retained evidence. They are
10/// zero when the scan used [`crate::EvidenceMode::SelectedFiles`], even when
11/// entries were excluded during selection.
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct ScanSummary {
15    pub selected_files: usize,
16    pub selected_bytes: u64,
17    pub hashed_files: usize,
18    pub binary_checked_files: usize,
19    pub recorded_skips: usize,
20    pub skipped_by_kind: BTreeMap<SkipKind, usize>,
21    pub warnings: usize,
22    pub ignore_sources: usize,
23    pub complete: bool,
24    pub termination: Option<ScanTermination>,
25    pub portable: bool,
26    pub cache: ScanCacheStats,
27}
28
29impl ScanSummary {
30    #[allow(clippy::too_many_arguments)]
31    fn from_parts(
32        selected_files: usize,
33        selected_bytes: impl Iterator<Item = u64>,
34        hashed_files: usize,
35        binary_checked_files: usize,
36        skipped: &[SkippedEntry],
37        warnings: usize,
38        ignore_sources: usize,
39        complete: bool,
40        termination: Option<ScanTermination>,
41        portable: bool,
42        cache: ScanCacheStats,
43    ) -> Self {
44        let mut skipped_by_kind = BTreeMap::new();
45        for entry in skipped {
46            *skipped_by_kind.entry(entry.kind).or_default() += 1;
47        }
48        Self {
49            selected_files,
50            selected_bytes: selected_bytes.fold(0_u64, u64::saturating_add),
51            hashed_files,
52            binary_checked_files,
53            recorded_skips: skipped.len(),
54            skipped_by_kind,
55            warnings,
56            ignore_sources,
57            complete,
58            termination,
59            portable,
60            cache,
61        }
62    }
63}
64
65impl ScanReport {
66    /// Aggregates this report without exposing repository paths.
67    #[must_use]
68    pub fn summary(&self) -> ScanSummary {
69        ScanSummary::from_parts(
70            self.files.len(),
71            self.files.iter().map(|file| file.bytes),
72            self.files
73                .iter()
74                .filter(|file| file.content_hash.is_some())
75                .count(),
76            self.files.iter().filter(|file| file.binary_checked).count(),
77            &self.skipped,
78            self.warnings.len(),
79            self.ignore_sources.len(),
80            self.complete,
81            self.termination,
82            self.portable,
83            self.cache,
84        )
85    }
86}
87
88impl CompactScanReport {
89    /// Aggregates this compact report without exposing repository paths.
90    #[must_use]
91    pub fn summary(&self) -> ScanSummary {
92        ScanSummary::from_parts(
93            self.files.len(),
94            self.files.iter().map(|file| file.bytes),
95            self.files
96                .iter()
97                .filter(|file| file.content_hash().is_some())
98                .count(),
99            self.files
100                .iter()
101                .filter(|file| {
102                    file.content
103                        .as_deref()
104                        .is_some_and(|content| content.binary_checked)
105                })
106                .count(),
107            &self.skipped,
108            self.warnings.len(),
109            self.ignore_sources.len(),
110            self.complete,
111            self.termination,
112            self.portable,
113            self.cache,
114        )
115    }
116}
117
118impl fmt::Display for ScanSummary {
119    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
120        write!(
121            formatter,
122            "files={} bytes={} hashed={} binary_checked={} skipped={} warnings={} \
123             ignore_sources={} complete={} portable={}",
124            self.selected_files,
125            self.selected_bytes,
126            self.hashed_files,
127            self.binary_checked_files,
128            self.recorded_skips,
129            self.warnings,
130            self.ignore_sources,
131            self.complete,
132            self.portable,
133        )?;
134        if let Some(termination) = self.termination {
135            write!(formatter, " termination={termination:?}")?;
136        }
137        if self.cache != ScanCacheStats::default() {
138            write!(
139                formatter,
140                " cache_reused_hashes={} cache_content_reads={} cache_fingerprint_reads={}",
141                self.cache.reused_hashes, self.cache.content_reads, self.cache.fingerprint_reads,
142            )?;
143        }
144        Ok(())
145    }
146}