malvin 0.2.6

Non-interactive research and coding agent
use std::path::Path;

use super::gate_restore_checks::substantive_check_lines;
use super::{
    DotfileBackupStateRef, GitignoreBackup, MalvinChecksBackup, MalvinConfigWorkspaceBackup,
    SessionDotfileBackups, VisionBackup,
};

pub(crate) const fn slot_deleted(
    anchor: DotfileBackupStateRef<'_>,
    progress: DotfileBackupStateRef<'_>,
) -> bool {
    matches!(anchor, DotfileBackupStateRef::Present(_))
        && matches!(progress, DotfileBackupStateRef::Missing)
}

pub(crate) const fn slot_bytes(value: DotfileBackupStateRef<'_>) -> Option<&[u8]> {
    match value {
        DotfileBackupStateRef::Present(payload) => Some(payload.bytes.as_slice()),
        DotfileBackupStateRef::Missing => None,
    }
}

pub(crate) fn slot_content_regressed(
    anchor: DotfileBackupStateRef<'_>,
    progress: DotfileBackupStateRef<'_>,
) -> bool {
    let (Some(anchor_bytes), Some(progress_bytes)) = (slot_bytes(anchor), slot_bytes(progress))
    else {
        return false;
    };
    anchor_bytes != progress_bytes
}

pub(crate) fn slot_regressed(
    anchor: DotfileBackupStateRef<'_>,
    progress: DotfileBackupStateRef<'_>,
) -> bool {
    slot_deleted(anchor, progress) || slot_content_regressed(anchor, progress)
}

pub(crate) fn checks_lines_are_superset(anchor_bytes: &[u8], progress_bytes: &[u8]) -> bool {
    let anchor_lines = substantive_check_lines(anchor_bytes);
    let progress_lines = substantive_check_lines(progress_bytes);
    anchor_lines
        .iter()
        .all(|line| progress_lines.iter().any(|p| p == line))
}

pub(crate) fn malvin_checks_regressed(
    anchor: &MalvinChecksBackup,
    progress: &MalvinChecksBackup,
) -> bool {
    let anchor_ref = anchor.as_slot_state();
    let progress_ref = progress.as_slot_state();
    if slot_deleted(anchor_ref, progress_ref) {
        return true;
    }
    let (Some(anchor_bytes), Some(progress_bytes)) =
        (slot_bytes(anchor_ref), slot_bytes(progress_ref))
    else {
        return false;
    };
    if anchor_bytes == progress_bytes {
        return false;
    }
    !checks_lines_are_superset(anchor_bytes, progress_bytes)
}

fn config_workspace_regressed(
    anchor: &MalvinConfigWorkspaceBackup,
    progress: &MalvinConfigWorkspaceBackup,
) -> bool {
    slot_regressed(anchor.as_slot_state(), progress.as_slot_state())
}

fn pick_typed<T: Clone>(anchor: &T, progress: &T, regress_probe: fn(&T, &T) -> bool) -> T {
    if regress_probe(anchor, progress) {
        anchor.clone()
    } else {
        progress.clone()
    }
}

pub(crate) fn root_file_bytes_by_name<'a>(
    files: impl IntoIterator<Item = (&'a Path, &'a [u8])>,
    root_name: &str,
) -> Option<&'a [u8]> {
    files
        .into_iter()
        .find(|(rel, _)| rel.as_os_str() == root_name)
        .map(|(_, bytes)| bytes)
}

pub(crate) const fn root_file_deleted_regressed(
    anchor_root: Option<&[u8]>,
    progress_root: Option<&[u8]>,
) -> bool {
    matches!((anchor_root, progress_root), (Some(_), None))
}

pub(crate) fn gitignore_root_bytes(backup: &GitignoreBackup) -> Option<&[u8]> {
    match backup {
        GitignoreBackup::Missing => None,
        GitignoreBackup::Present { files, .. } => root_file_bytes_by_name(
            files.iter().map(|file| (file.rel.as_path(), file.bytes.as_slice())),
            ".gitignore",
        ),
    }
}

fn gitignore_regressed(anchor: &GitignoreBackup, progress: &GitignoreBackup) -> bool {
    root_file_deleted_regressed(gitignore_root_bytes(anchor), gitignore_root_bytes(progress))
}

pub(crate) fn vision_root_bytes(backup: &VisionBackup) -> Option<&[u8]> {
    match backup {
        VisionBackup::Missing => None,
        VisionBackup::Present { files, .. } => root_file_bytes_by_name(
            files.iter().map(|file| (file.rel.as_path(), file.bytes.as_slice())),
            "VISION.md",
        ),
    }
}

fn vision_regressed(anchor: &VisionBackup, progress: &VisionBackup) -> bool {
    root_file_deleted_regressed(vision_root_bytes(anchor), vision_root_bytes(progress))
}

#[must_use]
pub fn merge_for_gate_restore(
    anchor: &SessionDotfileBackups,
    progress: &SessionDotfileBackups,
) -> SessionDotfileBackups {
    SessionDotfileBackups {
        malvin_checks: pick_typed(
            &anchor.malvin_checks,
            &progress.malvin_checks,
            malvin_checks_regressed,
        ),
        gitignore: pick_typed(&anchor.gitignore, &progress.gitignore, gitignore_regressed),
        vision: pick_typed(&anchor.vision, &progress.vision, vision_regressed),
        malvin_config_workspace: pick_typed(
            &anchor.malvin_config_workspace,
            &progress.malvin_config_workspace,
            config_workspace_regressed,
        ),
    }
}

#[must_use]
pub fn merge_and_sanitize_for_gate_restore(
    anchor: &SessionDotfileBackups,
    progress: &SessionDotfileBackups,
    work_dir: &Path,
) -> SessionDotfileBackups {
    let _ = work_dir;
    merge_for_gate_restore(anchor, progress)
}

#[cfg(test)]
#[path = "gate_restore_merge_tests.rs"]
mod gate_restore_merge_tests;