metactl 0.1.3

metactl v2 reference kernel and JSON-RPC service
Documentation
use std::fs;
use std::path::{Path, PathBuf};

use anyhow::{anyhow, Context, Result};
use serde::de::DeserializeOwned;
use sha2::{Digest, Sha256};

use crate::types::{
    CompileManifest, Config, EnforcementStatus, ExplainResult, InvocationOverlay, PackManifest,
    PolicyEnforcementReport, PolicyManifest, ProvenanceEnvelope, Ref, ResolveGraph, RoleManifest,
    SearchResult, TargetCapabilityMatrix, ValidateParams, ValidationCheck, ValidationReport,
    ValidationStatus,
};

#[derive(Debug, Clone)]
pub struct SuiteContext {
    pub name: String,
    pub root: PathBuf,
    pub repo_root: PathBuf,
    pub config: Config,
    pub role_manifest: RoleManifest,
    pub policy_manifest: PolicyManifest,
    pub target_capability: TargetCapabilityMatrix,
    pub packs: Vec<PackManifest>,
    pub provenance: Vec<ProvenanceEnvelope>,
    pub search_result: SearchResult,
    pub resolve_graph: ResolveGraph,
    pub explain_result: ExplainResult,
    pub compile_manifest: CompileManifest,
    pub policy_enforcement_report: PolicyEnforcementReport,
    pub validation_report: ValidationReport,
}

impl SuiteContext {
    pub fn selected_target(&self) -> Ref {
        self.target_capability.target_ref()
    }

    pub fn policy_ref(&self) -> Ref {
        self.policy_manifest.policy_ref()
    }

    pub fn role_ref(&self) -> Ref {
        self.role_manifest.role_ref()
    }

    pub fn matches_config(&self, config: &Config, overlay: Option<&InvocationOverlay>) -> bool {
        let target = selected_target_from_config(config, overlay);
        self.role_ref() == config.role
            && self.policy_ref() == config.policy
            && Some(self.selected_target()) == target
    }

    pub fn matches_graph(&self, graph: &ResolveGraph) -> bool {
        self.role_ref() == graph.role && self.selected_target() == graph.selected_target
    }

    pub fn materialize_compile_manifest(&self) -> Result<CompileManifest> {
        let mut manifest = self.compile_manifest.clone();
        for output in &mut manifest.generated_outputs {
            let path = self.repo_root.join(&output.path);
            if !path.exists() {
                return Err(anyhow!("missing generated output {}", output.path));
            }
            output.digest = Some(sha256_digest(&path)?);
        }
        Ok(manifest)
    }

    pub fn validation_report_for(
        &self,
        compile_manifest: &CompileManifest,
        policy_report: Option<&PolicyEnforcementReport>,
    ) -> Result<ValidationReport> {
        let mut report = self.validation_report.clone();
        let digest_ok = compile_manifest.generated_outputs.iter().all(|output| {
            let expected = output.digest.as_deref().unwrap_or_default();
            let path = self.repo_root.join(&output.path);
            path.exists()
                && sha256_digest(&path)
                    .map(|actual| actual == expected)
                    .unwrap_or(false)
        });

        if !digest_ok {
            report.status = ValidationStatus::Fail;
            upsert_check(
                &mut report,
                ValidationCheck {
                    id: "generated-output-digests".to_string(),
                    status: ValidationStatus::Fail,
                    message: "Generated outputs do not match recorded digests.".to_string(),
                    artifact_ref: None,
                },
            );
            return Ok(report);
        }

        if let Some(policy_report) = policy_report {
            let has_degraded = policy_report
                .rules
                .iter()
                .any(|rule| rule.status == EnforcementStatus::Degraded);
            if has_degraded && report.status == ValidationStatus::Pass {
                report.status = ValidationStatus::Warn;
            }
        }

        Ok(report)
    }
}

#[derive(Debug, Clone)]
pub struct SuiteRegistry {
    suites: Vec<SuiteContext>,
}

impl SuiteRegistry {
    pub fn load_from_dir(root: impl AsRef<Path>) -> Result<Self> {
        let root = root.as_ref();
        let repo_root = root
            .parent()
            .and_then(Path::parent)
            .ok_or_else(|| anyhow!("unable to infer repo root from {}", root.display()))?
            .to_path_buf();
        let mut suites = Vec::new();
        for entry in
            fs::read_dir(root).with_context(|| format!("read fixtures root {}", root.display()))?
        {
            let entry = entry?;
            if !entry.file_type()?.is_dir() {
                continue;
            }
            suites.push(Self::load_suite(&repo_root, &entry.path())?);
        }
        if suites.is_empty() {
            return Err(anyhow!("no suite directories found in {}", root.display()));
        }
        Ok(Self { suites })
    }

    pub fn suite_names(&self) -> Vec<String> {
        self.suites.iter().map(|suite| suite.name.clone()).collect()
    }

    pub fn find_by_config(
        &self,
        config: &Config,
        overlay: Option<&InvocationOverlay>,
    ) -> Result<&SuiteContext> {
        self.suites
            .iter()
            .find(|suite| suite.matches_config(config, overlay))
            .ok_or_else(|| {
                anyhow!(
                    "no suite matched role={} target={}",
                    config.role.id,
                    selected_target_from_config(config, overlay)
                        .map(|r| r.id)
                        .unwrap_or_else(|| "<none>".to_string())
                )
            })
    }

    pub fn find_by_graph(&self, graph: &ResolveGraph) -> Result<&SuiteContext> {
        self.suites
            .iter()
            .find(|suite| suite.matches_graph(graph))
            .ok_or_else(|| {
                anyhow!(
                    "no suite matched resolve graph role={} target={}",
                    graph.role.id,
                    graph.selected_target.id
                )
            })
    }

    pub fn find_for_validate(&self, params: &ValidateParams) -> Result<&SuiteContext> {
        if let Some(graph) = params.resolve_graph.as_ref() {
            return self.find_by_graph(graph);
        }
        let target = params
            .compile_manifest
            .as_ref()
            .map(|manifest| manifest.target.clone())
            .unwrap_or_else(|| params.subject_ref.clone());
        self.suites
            .iter()
            .find(|suite| suite.selected_target() == target)
            .ok_or_else(|| anyhow!("no suite matched validation subject {}", target.id))
    }

    fn load_suite(repo_root: &Path, dir: &Path) -> Result<SuiteContext> {
        let name = dir
            .file_name()
            .and_then(|value| value.to_str())
            .unwrap_or("unknown")
            .to_string();
        let config = load_json(dir.join("config.json"))?;
        let role_manifest = load_json(dir.join("role.manifest.json"))?;
        let policy_manifest = load_json(dir.join("policy.manifest.json"))?;
        let target_capability = load_json(dir.join("target.capability.json"))?;
        let search_result = load_json(dir.join("search.result.json"))?;
        let resolve_graph = load_json(dir.join("resolve.graph.json"))?;
        let explain_result = load_json(dir.join("explain.result.json"))?;
        let compile_manifest = load_json(dir.join("compile.manifest.json"))?;
        let policy_enforcement_report = load_json(dir.join("policy.enforcement.report.json"))?;
        let validation_report = load_json(dir.join("validation.report.json"))?;
        let provenance = load_json(dir.join("provenance.bundle.json"))?;

        let mut packs = Vec::new();
        for path in sorted_glob(dir, "pack.*.json")? {
            packs.push(load_json(path)?);
        }

        Ok(SuiteContext {
            name,
            root: dir.to_path_buf(),
            repo_root: repo_root.to_path_buf(),
            config,
            role_manifest,
            policy_manifest,
            target_capability,
            packs,
            provenance,
            search_result,
            resolve_graph,
            explain_result,
            compile_manifest,
            policy_enforcement_report,
            validation_report,
        })
    }
}

pub fn selected_target_from_config(
    config: &Config,
    overlay: Option<&InvocationOverlay>,
) -> Option<Ref> {
    overlay
        .and_then(|item| item.selected_target_override.clone())
        .or_else(|| config.targets.first().cloned())
}

fn load_json<T: DeserializeOwned>(path: PathBuf) -> Result<T> {
    let bytes = fs::read(&path).with_context(|| format!("read {}", path.display()))?;
    serde_json::from_slice(&bytes).with_context(|| format!("decode {}", path.display()))
}

fn sorted_glob(dir: &Path, pattern: &str) -> Result<Vec<PathBuf>> {
    let mut entries = dir
        .read_dir()
        .with_context(|| format!("read {}", dir.display()))?
        .filter_map(|entry| entry.ok().map(|item| item.path()))
        .filter(|path| {
            path.file_name()
                .and_then(|value| value.to_str())
                .is_some_and(|value| glob_match(pattern, value))
        })
        .collect::<Vec<_>>();
    entries.sort();
    Ok(entries)
}

fn glob_match(pattern: &str, value: &str) -> bool {
    let needle = pattern
        .strip_prefix("pack.")
        .and_then(|rest| rest.strip_suffix(".json"));
    if let Some(inner) = needle {
        value.starts_with("pack.")
            && value.ends_with(".json")
            && (inner == "*"
                || !value
                    .trim_start_matches("pack.")
                    .trim_end_matches(".json")
                    .is_empty())
    } else {
        false
    }
}

fn sha256_digest(path: &Path) -> Result<String> {
    let bytes = fs::read(path).with_context(|| format!("read {}", path.display()))?;
    let digest = Sha256::digest(bytes);
    Ok(format!("sha256:{}", hex::encode(digest)))
}

fn upsert_check(report: &mut ValidationReport, check: ValidationCheck) {
    if let Some(existing) = report.checks.iter_mut().find(|item| item.id == check.id) {
        *existing = check;
    } else {
        report.checks.push(check);
    }
}