weavatrix-scan 0.4.4

Deterministic, safe repository scanner for code intelligence
Documentation
use crate::config::{EvidenceMode, ScanOptions};
use crate::report::{ScanReport, ScanTermination, SkipKind, SkippedEntry};
use std::time::Instant;

pub(crate) struct ScanRuntime {
    pub(crate) started: Instant,
    entries: u64,
}

impl ScanRuntime {
    pub(crate) fn new() -> Self {
        Self {
            started: Instant::now(),
            entries: 0,
        }
    }

    pub(crate) fn before_next(&self, options: &ScanOptions) -> Option<ScanTermination> {
        if let Some(reason) = self.external_termination(options) {
            return Some(reason);
        }
        if options
            .limits
            .max_entries
            .is_some_and(|limit| self.entries >= limit)
        {
            return Some(ScanTermination::MaxEntries);
        }
        None
    }

    pub(crate) fn record_entry(&mut self) {
        self.entries = self.entries.saturating_add(1);
    }

    pub(crate) fn external_termination(&self, options: &ScanOptions) -> Option<ScanTermination> {
        if options
            .cancellation
            .as_ref()
            .is_some_and(crate::CancellationToken::is_cancelled)
        {
            return Some(ScanTermination::Cancelled);
        }
        if options
            .limits
            .timeout
            .is_some_and(|timeout| self.started.elapsed() >= timeout)
        {
            return Some(ScanTermination::Timeout);
        }
        None
    }
}

pub(crate) fn apply_total_bytes_limit(report: &mut ScanReport, options: &ScanOptions) {
    let Some(limit) = options.limits.max_total_bytes else {
        return;
    };
    report
        .files
        .sort_unstable_by(|left, right| left.relative.cmp(&right.relative));
    let mut total = 0_u64;
    let keep = report
        .files
        .iter()
        .position(|file| {
            let next = total.saturating_add(file.bytes);
            if next > limit {
                true
            } else {
                total = next;
                false
            }
        })
        .unwrap_or(report.files.len());
    if keep == report.files.len() {
        return;
    }
    let truncated = report.files.split_off(keep);
    if options.evidence == EvidenceMode::Complete {
        report
            .skipped
            .extend(truncated.into_iter().map(|file| SkippedEntry {
                relative: file.relative,
                kind: SkipKind::ScanLimit,
                detail: Some(format!("maximum selected bytes: {limit}")),
            }));
    }
    report.terminate(ScanTermination::MaxTotalBytes);
}