provenance-cli 0.2.1

Command-line interface for Provenance requirements traceability
use crate::output::{self, OutputFormat};
use camino::{Utf8Path, Utf8PathBuf};
use provenance_store::{layout::ProvenanceLayout, state_store::StateStore};
use std::collections::BTreeSet;

mod edges;
mod index;
mod references;
mod scope;
mod statement_report;

use index::CheckIndex;

pub(super) fn check(repo: &Utf8Path, format: OutputFormat) -> anyhow::Result<()> {
    let store = StateStore::new(ProvenanceLayout::new(repo.to_path_buf()));
    let report = store.with_repository_publication(|| collect_report_locked(&store, repo))?;
    output::print(format, &report)
}

#[derive(serde::Serialize)]
struct CheckReport {
    status: &'static str,
    diagnostics: Vec<provenance_store::statement_analysis::StatementDiagnostic>,
}

pub(super) fn validate_repository(repo: Utf8PathBuf) -> anyhow::Result<()> {
    let store = StateStore::new(ProvenanceLayout::new(repo));
    store.with_repository_publication(|| validate_locked(&store))
}

fn collect_report_locked(store: &StateStore, repo: &Utf8Path) -> anyhow::Result<CheckReport> {
    validate_locked(store)?;
    Ok(CheckReport {
        status: "ok",
        diagnostics: statement_report::changed_statements_from_head(store, repo)?,
    })
}

fn validate_locked(store: &StateStore) -> anyhow::Result<()> {
    let manifest = store.manifest()?;
    anyhow::ensure!(
        !manifest.scopes.is_empty(),
        "manifest must contain at least one scope"
    );
    let manifest_scopes: BTreeSet<_> = manifest
        .scopes
        .iter()
        .map(|scope| scope.id.as_str().to_string())
        .collect();

    let scope_directory_findings = store
        .list_scope_directories()?
        .into_iter()
        .filter(|directory| !manifest_scopes.contains(directory))
        .map(|directory| format!("scope directory {directory} is absent from manifest"))
        .collect::<Vec<_>>();

    let mut index = CheckIndex::default();
    let mut dangling = Vec::new();
    scope::validate(
        store,
        &manifest.scopes,
        &manifest_scopes,
        &mut index,
        &mut dangling,
    )?;
    edges::validate(store, &manifest_scopes, &index, &mut dangling)?;

    anyhow::ensure!(
        scope_directory_findings.is_empty(),
        "scope directory finding(s):\n- {}",
        scope_directory_findings.join("\n- ")
    );
    anyhow::ensure!(
        dangling.is_empty(),
        "dangling reference(s):\n- {}",
        dangling.join("\n- ")
    );
    Ok(())
}