visual-rubric 0.2.0

AI-assisted screenshot rubric runner for local visual UX review
Documentation
use std::fs;
use std::path::{Component, Path};

use crate::{LogCaptureConfig, LogPathMode};

pub(super) fn copy_logs_from_config(
    config: Option<&LogCaptureConfig>,
) -> std::io::Result<Vec<String>> {
    let Some(config) = config else {
        return Ok(Vec::new());
    };
    let mut copied = Vec::new();
    copy_selected_logs(
        &config.temp_dir,
        &config.temp_dir,
        &config.output_dir,
        &config.path_mode,
        &mut copied,
    )?;
    copied.sort();
    Ok(copied)
}

fn copy_selected_logs(
    source_root: &Path,
    current_source: &Path,
    destination_root: &Path,
    path_mode: &LogPathMode,
    copied: &mut Vec<String>,
) -> std::io::Result<()> {
    if !current_source.exists() {
        return Ok(());
    }
    for entry in fs::read_dir(current_source)? {
        let entry = entry?;
        let source = entry.path();
        if source.is_dir() {
            copy_selected_logs(source_root, &source, destination_root, path_mode, copied)?;
        } else if should_copy_acp_log(&source) {
            let relative = source
                .strip_prefix(source_root)
                .map_or(source.as_path(), |relative| relative);
            let destination = destination_root.join(relative);
            if let Some(parent) = destination.parent() {
                fs::create_dir_all(parent)?;
            }
            fs::copy(&source, &destination)?;
            copied.push(report_path(path_mode, &destination));
        }
    }
    Ok(())
}

fn should_copy_acp_log(path: &Path) -> bool {
    let file_name = path
        .file_name()
        .and_then(|name| name.to_str())
        .unwrap_or_default();
    file_name.starts_with("logs_")
        || file_name.ends_with(".jsonl") && path.components().any(is_sessions_component)
}

fn is_sessions_component(component: Component<'_>) -> bool {
    matches!(component, Component::Normal(value) if value == "sessions")
}

fn report_path(path_mode: &LogPathMode, destination: &Path) -> String {
    match path_mode {
        LogPathMode::RelativeTo(root) => destination
            .strip_prefix(root)
            .map_or(destination, |relative| relative)
            .to_string_lossy()
            .into_owned(),
        LogPathMode::Absolute => destination.to_string_lossy().into_owned(),
    }
}