ccd-cli 1.0.0-beta.4

Bootstrap and validate Continuous Context Development repositories
use std::path::Path;
use std::process::ExitCode;

use anyhow::{bail, Result};
use serde::Serialize;

use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::profile;
use crate::repo::marker as repo_marker;
use crate::state::escalation as escalation_state;
use crate::state::machine_registry as pod_identity;
use crate::state::policy_projection;

#[derive(Serialize)]
pub struct PolicyCheckReport {
    command: &'static str,
    ok: bool,
    path: String,
    profile: String,
    project_id: String,
    locality_id: String,
    action_family: policy_projection::PolicyActionFamily,
    policy_posture: policy_projection::AutonomousPolicyPosture,
    decision: policy_projection::PolicyDecision,
    reason_code: &'static str,
    summary: String,
    policy_projection: policy_projection::PolicyProjectionView,
}

impl CommandReport for PolicyCheckReport {
    fn exit_code(&self) -> ExitCode {
        ExitCode::SUCCESS
    }

    fn render_text(&self) {
        println!(
            "{} -> {} ({})",
            self.action_family.label(),
            self.decision_label(),
            self.reason_code
        );
        println!("{}", self.summary);
    }
}

impl PolicyCheckReport {
    fn decision_label(&self) -> &'static str {
        match self.decision {
            policy_projection::PolicyDecision::Allow => "allow",
            policy_projection::PolicyDecision::ApprovalRequired => "approval_required",
            policy_projection::PolicyDecision::EscalationRequired => "escalation_required",
            policy_projection::PolicyDecision::Deny => "deny",
        }
    }
}

pub fn run(
    repo_root: &Path,
    explicit_profile: Option<&str>,
    action_family: policy_projection::PolicyActionFamily,
) -> Result<PolicyCheckReport> {
    let profile = profile::resolve(explicit_profile)?;
    let layout = StateLayout::resolve(repo_root, profile.clone())?;
    let Some(marker) = repo_marker::load(repo_root)? else {
        bail!(
            "workspace is not linked: {} is missing; run `ccd attach --path {}` or `ccd link --path {}` first",
            repo_root.join(repo_marker::MARKER_FILE).display(),
            repo_root.display(),
            repo_root.display()
        );
    };

    let active_pod_identity = pod_identity::resolve_active_identity(&layout)?;
    let policy_sources = policy_projection::read_policy_sources(
        &layout,
        &marker.locality_id,
        active_pod_identity.as_ref(),
    )?;
    let session_context = policy_projection::load_session_context(&layout)?;
    let escalation_entries = escalation_state::load_for_layout(&layout)?;
    let escalation = escalation_state::build_view(&layout, &escalation_entries);
    let projection = policy_projection::build_view(&policy_sources, session_context, &escalation);
    let evaluation = policy_projection::evaluate_action_family(&projection, action_family);

    Ok(PolicyCheckReport {
        command: "policy-check",
        ok: true,
        path: repo_root.display().to_string(),
        profile: profile.to_string(),
        project_id: marker.locality_id.clone(),
        locality_id: marker.locality_id,
        action_family,
        policy_posture: projection.autonomous_default_posture,
        decision: evaluation.decision,
        reason_code: evaluation.reason_code,
        summary: evaluation.summary,
        policy_projection: projection,
    })
}