weavatrix_scan/content_visit.rs
1use crate::report::{
2 CompactScanReport, CompactScannedFile, IgnoreSourceEvidence, ScanCacheStats, ScanTermination,
3 ScanWarning, SkippedEntry,
4};
5use std::path::{Path, PathBuf};
6
7/// Controls whether a content visit retains selected-file evidence internally.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ContentVisitMode {
10 /// Retain compact selected-file evidence and compute a deterministic
11 /// revision.
12 Revision,
13 /// Emit bytes and counters without retaining selected-file evidence or
14 /// computing a revision.
15 Streaming,
16}
17
18/// Controls delivery from [`crate::Scanner::visit_content`].
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum ContentVisitControl {
21 /// Continue delivering the current file.
22 Continue,
23 /// Stop delivering chunks for this file while allowing required scanner
24 /// evidence work to finish.
25 SkipFile,
26 /// Cooperatively stop every content worker.
27 Quit,
28}
29
30/// Stable identity and discovery evidence for one selected file.
31#[derive(Debug, Clone, Copy)]
32pub struct ContentFile<'a> {
33 /// Root insertion index. A single-root [`crate::Scanner`] always uses zero.
34 pub root_index: usize,
35 /// Monotonic work sequence within this visit. Sort by `root_index` and
36 /// `relative` when results must be deterministic across runs.
37 pub sequence: u64,
38 pub root: &'a Path,
39 pub absolute: &'a Path,
40 pub relative: &'a str,
41 pub bytes: u64,
42}
43
44/// Result of verifying the file around its single content read.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum ContentFileStatus {
47 Selected,
48 Binary,
49 Changed,
50}
51
52/// Events emitted from one bounded content worker.
53#[derive(Debug)]
54pub enum ContentVisitEvent<'a> {
55 /// The selected file was opened and its discovery evidence was verified.
56 FileStart {
57 worker_index: usize,
58 file: ContentFile<'a>,
59 },
60 /// A borrowed chunk from the scanner's one content read.
61 Chunk {
62 worker_index: usize,
63 file: ContentFile<'a>,
64 offset: u64,
65 bytes: &'a [u8],
66 },
67 /// The file read and post-read verification finished.
68 FileEnd {
69 worker_index: usize,
70 file: ContentFile<'a>,
71 status: ContentFileStatus,
72 bytes_read: u64,
73 content_hash: Option<&'a str>,
74 consumer_skipped: bool,
75 },
76}
77
78/// Summary of a parallel, one-pass selected-content visit.
79#[derive(Debug)]
80pub struct ContentVisitReport {
81 pub mode: ContentVisitMode,
82 pub root: PathBuf,
83 /// Deterministic selected-file manifest retained in revision mode.
84 ///
85 /// Every entry carries the content hash and file-version evidence produced
86 /// by the same read that emitted its bytes to the visitor. Streaming mode
87 /// leaves this empty.
88 pub files: Vec<CompactScannedFile>,
89 /// Candidates selected by traversal and ignore rules before content checks.
90 pub discovered: u64,
91 /// Files that completed content checks and remain selected.
92 pub completed: u64,
93 pub opened: u64,
94 pub chunks: u64,
95 pub bytes_read: u64,
96 pub bytes_emitted: u64,
97 pub consumer_skipped: u64,
98 pub stopped: bool,
99 pub skipped: Vec<SkippedEntry>,
100 pub warnings: Vec<ScanWarning>,
101 pub ignore_sources: Vec<IgnoreSourceEvidence>,
102 pub revision: String,
103 pub complete: bool,
104 pub termination: Option<ScanTermination>,
105 pub portable: bool,
106 pub cache: ScanCacheStats,
107}
108
109impl ContentVisitReport {
110 /// Converts a revision-mode visit into the compact scanner manifest used
111 /// by incremental consumers.
112 ///
113 /// Streaming visits deliberately do not retain file evidence and therefore
114 /// cannot be converted.
115 ///
116 /// # Errors
117 ///
118 /// Returns an error when called for a streaming visit.
119 pub fn into_compact_scan_report(self) -> Result<CompactScanReport, &'static str> {
120 if self.mode != ContentVisitMode::Revision {
121 return Err("streaming content visits do not retain a file manifest");
122 }
123 Ok(CompactScanReport {
124 root: self.root,
125 files: self.files,
126 skipped: self.skipped,
127 warnings: self.warnings,
128 ignore_sources: self.ignore_sources,
129 revision: self.revision,
130 complete: self.complete,
131 termination: self.termination,
132 portable: self.portable,
133 cache: self.cache,
134 })
135 }
136}
137
138/// Ordered summaries from a multi-root content visit.
139#[derive(Debug)]
140pub struct MultiContentVisitReport {
141 /// Reports remain in the same order as roots were added.
142 pub reports: Vec<ContentVisitReport>,
143}
144
145impl MultiContentVisitReport {
146 #[must_use]
147 pub const fn len(&self) -> usize {
148 self.reports.len()
149 }
150
151 #[must_use]
152 pub const fn is_empty(&self) -> bool {
153 self.reports.is_empty()
154 }
155}
156
157/// Content and removals produced by a safe file-only watcher plan.
158#[derive(Debug)]
159pub struct ChangedContentVisitReport {
160 /// Evidence for the changed files that still exist and remain selected.
161 ///
162 /// Its revision describes this changed-file subset, not the complete
163 /// repository manifest.
164 pub content: ContentVisitReport,
165 /// Stable normalized paths that disappeared from the repository.
166 pub removed: Vec<String>,
167}
168
169/// Result of attempting a traversal-free watcher content visit.
170#[derive(Debug)]
171pub enum ChangedContentVisitOutcome {
172 /// Only changed file paths were matched, opened, and visited.
173 Visited(Box<ChangedContentVisitReport>),
174 /// The plan can affect directory structure or selection and therefore
175 /// requires the caller to perform a complete scan.
176 FullRescanRequired,
177}