tuff-core 0.10.2

Core lifecycle engine and adapter contract for Tuff.
Documentation
use std::path::Path;

use serde::Serialize;

use crate::error::Result;
use crate::lockfile;
use crate::manifest::CapabilityType;

#[derive(Debug, Serialize)]
pub struct CheckResult {
    pub id: String,
    #[serde(rename = "type")]
    pub capability_type: CapabilityType,
    pub target: String,
    pub status: String,
    #[serde(skip_serializing_if = "Vec::is_empty")]
    pub files: Vec<String>,
}

#[derive(Debug, Serialize)]
pub struct CheckOutcome {
    pub valid: bool,
    pub results: Vec<CheckResult>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CheckScope {
    ProjectAndGlobal,
    Global,
}

pub fn run_checks(repo_root: &Path, scope: CheckScope) -> Result<CheckOutcome> {
    let mut results = Vec::new();

    if scope == CheckScope::ProjectAndGlobal
        && let Some(lf) = lockfile::read_optional_lockfile(&lockfile::project_lockfile(repo_root))?
    {
        check_lockfile(repo_root, &lf, &mut results);
    }

    if let Some(home) = home_dir() {
        let lock_path = crate::paths::global_lockfile(&home);
        if let Some(lf) = lockfile::read_optional_lockfile(&lock_path)? {
            check_lockfile(&home, &lf, &mut results);
        }
    }

    let valid = results.iter().all(|r| r.status == "ok");
    Ok(CheckOutcome { valid, results })
}

fn check_lockfile(scope_root: &Path, lf: &lockfile::Lockfile, results: &mut Vec<CheckResult>) {
    for (id, entry) in lf.capabilities.iter() {
        for (target_id, target_entry) in entry.targets.iter() {
            let mut failing_files = Vec::new();

            if target_entry.installed_path.is_empty() {
                failing_files.push(id.clone());
            } else {
                let path = scope_root.join(&target_entry.installed_path);
                match crate::cache::hash_tree(&path) {
                    Ok(hash) if hash == target_entry.sha256 => {}
                    Ok(_) | Err(_) => failing_files.push(target_entry.installed_path.clone()),
                }
            }

            for hook in &target_entry.managed_hooks {
                if lockfile::managed_hook_status(scope_root, hook) != "clean" {
                    failing_files.push(format!("{}#{}", hook.settings_path, hook.event));
                }
            }

            for permission in &target_entry.managed_permissions {
                if crate::policy::managed_permission_status(scope_root, permission) != "clean" {
                    let location = format!(
                        "{}#permissions.{}",
                        permission.settings_path, permission.list
                    );
                    if !failing_files.contains(&location) {
                        failing_files.push(location);
                    }
                }
            }

            if let Some(managed_entry) = &target_entry.managed_mcp_entry
                && lockfile::managed_mcp_entry_status(scope_root, id, managed_entry) != "clean"
            {
                failing_files.push(format!("{}#{}", managed_entry.config_path, id));
            }

            let status = if failing_files.is_empty() {
                "ok"
            } else {
                "modified"
            };

            results.push(CheckResult {
                id: id.clone(),
                capability_type: entry.capability_type,
                target: target_id.clone(),
                status: status.to_string(),
                files: failing_files,
            });
        }
    }
}

fn home_dir() -> Option<std::path::PathBuf> {
    std::env::var("HOME").ok().map(std::path::PathBuf::from)
}