ccd-cli 1.0.0-beta.2

Bootstrap and validate Continuous Context Development repositories
use crate::session_boundary::SessionBoundaryAction;
use crate::state::escalation as escalation_state;
use crate::state::runtime as runtime_state;
use crate::state::session as session_state;

use super::{
    ContextCheckAction, ContextCheckPayload, ContextCheckTrigger, ContextCheckUrgency,
    DriftAggregateStatus, NextStepStatus, RadarStateReport,
};

pub(super) struct ContextCheckDecision {
    pub(super) action: ContextCheckAction,
    pub(super) reason: &'static str,
    pub(super) recommendation: String,
    pub(super) urgency: ContextCheckUrgency,
    pub(super) suppressed: bool,
    pub(super) next_interval_hint_seconds: u64,
    pub(super) evidence: Vec<String>,
}

pub(super) fn build_context_check_decision(
    trigger: ContextCheckTrigger,
    report: &RadarStateReport,
    payload: &ContextCheckPayload,
) -> ContextCheckDecision {
    if let Some(checkout_state) = report
        .checkout_state
        .as_ref()
        .filter(|state| state.advisory)
    {
        return ContextCheckDecision {
            action: ContextCheckAction::Escalate,
            reason: "checkout_advisory",
            recommendation: "seek_operator_attention".to_owned(),
            urgency: ContextCheckUrgency::High,
            suppressed: false,
            next_interval_hint_seconds: 0,
            evidence: vec![checkout_state.summary.clone()],
        };
    }

    if report.escalation.blocking_count > 0 {
        return ContextCheckDecision {
            action: ContextCheckAction::Escalate,
            reason: "blocking_escalation",
            recommendation: "seek_operator_attention".to_owned(),
            urgency: ContextCheckUrgency::Critical,
            suppressed: false,
            next_interval_hint_seconds: 0,
            evidence: report
                .escalation
                .entries
                .iter()
                .filter(|entry| matches!(entry.kind, escalation_state::EscalationKind::Blocking))
                .map(|entry| format!("{}: {}", entry.id, entry.reason))
                .collect(),
        };
    }

    if report.behavioral_drift.status == DriftAggregateStatus::NeedsRecalibration {
        return ContextCheckDecision {
            action: ContextCheckAction::Escalate,
            reason: "behavioral_drift",
            recommendation: "recalibrate_session".to_owned(),
            urgency: ContextCheckUrgency::High,
            suppressed: false,
            next_interval_hint_seconds: 0,
            evidence: report.behavioral_drift.evidence.clone(),
        };
    }

    if matches!(
        trigger,
        ContextCheckTrigger::PreCompaction | ContextCheckTrigger::IdleReset
    ) {
        let reason = match trigger {
            ContextCheckTrigger::PreCompaction => "pre_compaction",
            ContextCheckTrigger::IdleReset => "idle_reset",
            _ => unreachable!("guarded above"),
        };
        let mut evidence = vec![payload.risk_summary.clone()];
        evidence.extend(report.session_boundary.evidence.iter().take(2).cloned());
        return ContextCheckDecision {
            action: ContextCheckAction::FlushForCompaction,
            reason,
            recommendation: "capture_before_compaction".to_owned(),
            urgency: ContextCheckUrgency::High,
            suppressed: false,
            next_interval_hint_seconds: 0,
            evidence,
        };
    }

    if matches!(
        report.context_health.recommendation,
        "wrap_up_soon" | "wrap_up_and_clear"
    ) {
        return ContextCheckDecision {
            action: ContextCheckAction::WrapUpRequired,
            reason: "wrap_up_window",
            recommendation: report.context_health.recommendation.to_owned(),
            urgency: match report.context_health.recommendation {
                "wrap_up_and_clear" => ContextCheckUrgency::Critical,
                _ => ContextCheckUrgency::High,
            },
            suppressed: false,
            next_interval_hint_seconds: 0,
            evidence: if report.session_boundary.evidence.is_empty() {
                vec![payload.risk_summary.clone()]
            } else {
                report.session_boundary.evidence.clone()
            },
        };
    }

    if report.session_boundary.action == SessionBoundaryAction::Refresh
        || report.evaluation.next_step.status == NextStepStatus::ReviewRequired
    {
        return ContextCheckDecision {
            action: ContextCheckAction::CheckpointNow,
            reason: "handoff_refresh_needed",
            recommendation: "checkpoint_now".to_owned(),
            urgency: ContextCheckUrgency::Moderate,
            suppressed: false,
            next_interval_hint_seconds: 0,
            evidence: if report.evaluation.next_step.evidence.is_empty() {
                vec![payload.state_delta.clone()]
            } else {
                report.evaluation.next_step.evidence.clone()
            },
        };
    }

    let manualish_trigger = matches!(
        trigger,
        ContextCheckTrigger::Manual
            | ContextCheckTrigger::Resume
            | ContextCheckTrigger::SupervisorPoll
    );
    let moderate_pressure = matches!(report.context_health.band, "moderate" | "risky")
        || report.execution_gates.unfinished_count > 0
        || report.recovery.status == "loaded";
    if manualish_trigger || moderate_pressure {
        return ContextCheckDecision {
            action: ContextCheckAction::InjectDelta,
            reason: match trigger {
                ContextCheckTrigger::Resume => "resume_refresh",
                ContextCheckTrigger::SupervisorPoll => "supervisor_poll",
                ContextCheckTrigger::Manual => "manual_refresh",
                _ => "band_transition",
            },
            recommendation: match trigger {
                ContextCheckTrigger::Resume => "resume_with_digest".to_owned(),
                _ => report.context_health.recommendation.to_owned(),
            },
            urgency: if matches!(report.context_health.band, "risky") {
                ContextCheckUrgency::High
            } else {
                ContextCheckUrgency::Moderate
            },
            suppressed: false,
            next_interval_hint_seconds: if matches!(trigger, ContextCheckTrigger::Resume) {
                300
            } else {
                600
            },
            evidence: vec![payload.state_delta.clone(), payload.risk_summary.clone()],
        };
    }

    ContextCheckDecision {
        action: ContextCheckAction::None,
        reason: if trigger == ContextCheckTrigger::Interval {
            "interval_suppressed"
        } else {
            "no_action"
        },
        recommendation: "continue".to_owned(),
        urgency: ContextCheckUrgency::Low,
        suppressed: trigger == ContextCheckTrigger::Interval,
        next_interval_hint_seconds: 900,
        evidence: vec![payload.risk_summary.clone()],
    }
}

pub(super) fn build_policy_digest(runtime: &runtime_state::LoadedRuntimeState) -> String {
    let guardrails: Vec<String> = runtime
        .state
        .handoff
        .operational_guardrails
        .iter()
        .map(|item| item.text.clone())
        .collect();
    if guardrails.is_empty() {
        return "No compiled operational guardrails are currently recorded.".to_owned();
    }

    summarize_lines(&guardrails, 2)
}

pub(super) fn build_focus_digest(report: &RadarStateReport) -> String {
    let mut parts = vec![report.handoff.title.clone()];
    if let Some(mode) = report
        .session_state
        .mode
        .filter(|mode| *mode != session_state::SessionMode::General)
    {
        parts.push(format!("mode `{}`", mode.as_str()));
    }
    if let Some(anchor) = &report.execution_gates.attention_anchor {
        parts.push(format!(
            "gate [{} #{}/{}] {}",
            anchor.status.as_str(),
            anchor.index,
            report.execution_gates.total_count,
            anchor.text
        ));
    } else if let Some(next_action) = report.handoff.immediate_actions.first() {
        parts.push(format!("next `{next_action}`"));
    }

    parts.join("; ")
}

pub(super) fn build_state_delta(report: &RadarStateReport) -> String {
    let mut parts = Vec::new();
    if !report.candidate_updates.handoff.is_empty() {
        parts.push(format!(
            "{} handoff update candidate(s)",
            report.candidate_updates.handoff.len()
        ));
    }
    if !report.candidate_updates.memory.is_empty() {
        parts.push(format!(
            "{} memory promotion candidate(s)",
            report.candidate_updates.memory.len()
        ));
    }
    if let Some(anchor) = &report.execution_gates.attention_anchor {
        parts.push(format!(
            "execution gate [{} #{}/{}] {}",
            anchor.status.as_str(),
            anchor.index,
            report.execution_gates.total_count,
            anchor.text
        ));
    }
    if parts.is_empty() {
        parts.push(report.evaluation.next_step.summary.clone());
    }
    parts.join("; ")
}

pub(super) fn build_risk_summary(
    trigger: ContextCheckTrigger,
    report: &RadarStateReport,
) -> String {
    let mut parts = vec![format!(
        "trigger `{}` with context band `{}` (`{}`)",
        trigger.as_str(),
        report.context_health.band,
        report.context_health.recommendation
    )];
    if report.behavioral_drift.status == DriftAggregateStatus::NeedsRecalibration {
        parts.push("behavioral drift needs recalibration".to_owned());
    }
    if report.escalation.blocking_count > 0 {
        parts.push(format!(
            "{} blocking escalation(s) active",
            report.escalation.blocking_count
        ));
    }
    parts.push(format!(
        "session boundary is `{}`",
        report.session_boundary.action.as_str()
    ));
    parts.join("; ")
}

fn summarize_lines(lines: &[String], limit: usize) -> String {
    let mut parts: Vec<String> = lines.iter().take(limit).cloned().collect();
    if lines.len() > limit {
        parts.push(format!("+{} more", lines.len() - limit));
    }
    parts.join("; ")
}