ccd-cli 1.0.0-beta.3

Bootstrap and validate Continuous Context Development repositories
use crate::handoff;
use crate::session_boundary::{SessionBoundaryAction, SessionBoundaryRecommendation};
use crate::state::escalation as escalation_state;
use crate::state::runtime as runtime_state;
use crate::state::work_stream_decay;

use super::{
    ApprovalStep, BehavioralDriftState, CandidateUpdates, ContextHealth, DriftAggregateStatus,
    NextStepStatus, RadarEvaluation, WrapUpStatus,
};

pub(super) fn build_session_boundary(
    checkout_state: Option<&handoff::CheckoutStateView>,
    recovery: &runtime_state::RuntimeRecoveryView,
    work_stream_decay: &work_stream_decay::WorkStreamDecayView,
    context_health: &ContextHealth,
    behavioral_drift: &BehavioralDriftState,
    evaluation: &RadarEvaluation,
    escalation_view: &escalation_state::EscalationView,
) -> SessionBoundaryRecommendation {
    let recovery_note = (recovery.status == "loaded").then_some(
        "Recovery artifacts are loaded as supporting context only; handoff and execution gates remain authoritative."
            .to_owned(),
    );

    if let Some(checkout_state) = checkout_state.filter(|state| state.advisory) {
        let mut evidence = vec![checkout_state.summary.clone()];
        append_recovery_note(&mut evidence, recovery_note.as_deref());
        return SessionBoundaryRecommendation::new(
            SessionBoundaryAction::Stop,
            "Stop here and continue from a live checkout before trusting this session boundary.",
            evidence,
        );
    }

    if escalation_view.blocking_count > 0 {
        let mut evidence = escalation_view
            .entries
            .iter()
            .filter(|entry| matches!(entry.kind, escalation_state::EscalationKind::Blocking))
            .map(|entry| format!("{}: {}", entry.id, entry.reason))
            .collect::<Vec<_>>();
        append_recovery_note(&mut evidence, recovery_note.as_deref());
        return SessionBoundaryRecommendation::new(
            SessionBoundaryAction::Stop,
            "Stop and resolve blocking escalations before treating Radar wrap-up as authoritative.",
            evidence,
        );
    }

    if work_stream_decay.status == "critical" {
        let mut evidence = vec![format!(
            "The active work stream recorded {} consecutive no-progress attempts.",
            work_stream_decay.consecutive_no_progress
        )];
        if let Some(last_outcome) = work_stream_decay.last_outcome.as_deref() {
            evidence.push(format!("Latest attempt outcome: {last_outcome}."));
        }
        append_recovery_note(&mut evidence, recovery_note.as_deref());
        return SessionBoundaryRecommendation::new(
            SessionBoundaryAction::WrapUp,
            "Wrap up now and restart from a fresh session boundary; the active work stream is in a low-quality retry loop.",
            evidence,
        );
    }

    if behavioral_drift.status == DriftAggregateStatus::NeedsRecalibration {
        let mut evidence = behavioral_drift.evidence.clone();
        append_recovery_note(&mut evidence, recovery_note.as_deref());
        return SessionBoundaryRecommendation::new(
            SessionBoundaryAction::Stop,
            "Stop and recalibrate the handoff or focus before closing this session.",
            evidence,
        );
    }

    if evaluation.next_step.status == NextStepStatus::ReviewRequired {
        let mut evidence = evaluation.next_step.evidence.clone();
        append_recovery_note(&mut evidence, recovery_note.as_deref());
        return SessionBoundaryRecommendation::new(
            SessionBoundaryAction::Refresh,
            "Refresh the workspace-local handoff before closing this session.",
            evidence,
        );
    }

    if evaluation.wrap_up.status == WrapUpStatus::NeedsReview {
        let mut evidence = evaluation.wrap_up.evidence.clone();
        append_recovery_note(&mut evidence, recovery_note.as_deref());
        return SessionBoundaryRecommendation::new(
            SessionBoundaryAction::Stop,
            evaluation.wrap_up.summary.clone(),
            evidence,
        );
    }

    if matches!(
        context_health.recommendation,
        "wrap_up_soon" | "wrap_up_and_clear"
    ) || matches!(
        evaluation.wrap_up.status,
        WrapUpStatus::ReadyToCommit | WrapUpStatus::ReadyToPush
    ) {
        let mut evidence = evaluation.wrap_up.evidence.clone();
        evidence.push(format!(
            "Context health recommends `{}` (band `{}`).",
            context_health.recommendation, context_health.band
        ));
        append_recovery_note(&mut evidence, recovery_note.as_deref());
        return SessionBoundaryRecommendation::new(
            SessionBoundaryAction::WrapUp,
            "Wrap up now; CCD sees a clean session boundary for close-out work.",
            evidence,
        );
    }

    let mut evidence = evaluation.next_step.evidence.clone();
    if evidence.is_empty() {
        evidence.push(format!(
            "Context health recommends `{}` (band `{}`).",
            context_health.recommendation, context_health.band
        ));
    }
    append_recovery_note(&mut evidence, recovery_note.as_deref());
    SessionBoundaryRecommendation::new(
        SessionBoundaryAction::Continue,
        "Continue the current session; CCD does not yet see a stronger boundary action.",
        evidence,
    )
}

fn append_recovery_note(evidence: &mut Vec<String>, note: Option<&str>) {
    if let Some(note) = note {
        evidence.push(note.to_owned());
    }
}

pub(super) fn build_approval_steps(
    evaluation: &RadarEvaluation,
    candidates: &CandidateUpdates,
    behavioral_drift: &BehavioralDriftState,
) -> Vec<ApprovalStep> {
    let mut steps = vec![ApprovalStep {
        id: "next_step",
        question: "Did this session change what should be next, or block something?",
        recommended_answer: if evaluation.next_step.status == NextStepStatus::ReviewRequired {
            "yes"
        } else {
            "no"
        },
        recommendation: evaluation.next_step.summary.clone(),
        evidence: evaluation.next_step.evidence.clone(),
    }];

    if behavioral_drift.status == DriftAggregateStatus::NeedsRecalibration {
        steps.push(ApprovalStep {
            id: "recalibrate",
            question:
                "Recent behavior drifted from CCD expectations. Do you want me to recalibrate the handoff or focus before wrap-up?",
            recommended_answer: "yes",
            recommendation: behavioral_drift.summary.clone(),
            evidence: behavioral_drift.evidence.clone(),
        });
    }

    if !candidates.memory.is_empty() {
        steps.push(ApprovalStep {
            id: "memory",
            question:
                "These are things worth persisting in MEMORY.md. Do you want me to persist them?",
            recommended_answer: "yes",
            recommendation: evaluation.memory.summary.clone(),
            evidence: candidates
                .memory
                .iter()
                .map(|candidate| candidate.summary.clone())
                .collect(),
        });
    }

    steps.push(ApprovalStep {
        id: "wrap_up",
        question: "Wrap up now? I can validate, show git status/diff, stage explicit paths, commit after approval, and push after approval.",
        recommended_answer: match evaluation.wrap_up.status {
            WrapUpStatus::ReadyToCommit | WrapUpStatus::ReadyToPush => "yes",
            WrapUpStatus::Clean
            | WrapUpStatus::NeedsReview
            | WrapUpStatus::NoChange => "no",
        },
        recommendation: evaluation.wrap_up.summary.clone(),
        evidence: evaluation.wrap_up.evidence.clone(),
    });

    steps
}