use crate::handoff::GitState;
use crate::state::session_gates;
use super::{
DriftAggregateStatus, EvaluationBucket, EvaluationInputs, MemoryStatus, NextStepStatus,
RadarEvaluation, WrapUpStatus,
};
pub(super) fn build_evaluation(inputs: EvaluationInputs<'_>) -> RadarEvaluation {
RadarEvaluation {
next_step: build_next_step_bucket(&inputs),
memory: build_memory_bucket(&inputs),
wrap_up: build_wrap_up_bucket(&inputs),
}
}
fn build_next_step_bucket(inputs: &EvaluationInputs<'_>) -> EvaluationBucket<NextStepStatus> {
if !inputs.candidates.handoff.is_empty() {
return EvaluationBucket {
status: NextStepStatus::ReviewRequired,
summary:
"Repo state changed relative to the recorded handoff; refresh the workspace-local handoff before closing."
.to_owned(),
evidence: inputs
.candidates
.handoff
.iter()
.map(|candidate| candidate.summary.clone())
.collect(),
};
}
match inputs.execution_gates.attention_anchor.as_ref() {
Some(anchor) if anchor.status == session_gates::ExecutionGateStatus::Blocked => {
EvaluationBucket {
status: NextStepStatus::ReviewRequired,
summary:
"The first unfinished execution gate is blocked; resolve it or rewrite the gate set before clean wrap-up."
.to_owned(),
evidence: execution_gate_evidence(inputs.execution_gates),
}
}
Some(_) => EvaluationBucket {
status: NextStepStatus::Continue,
summary:
"Execution gates are active; keep working through the first unfinished gate before clean wrap-up."
.to_owned(),
evidence: execution_gate_evidence(inputs.execution_gates),
},
None => EvaluationBucket {
status: NextStepStatus::NoChangeDetected,
summary: "No deterministic CLI signal says the next step changed.".to_owned(),
evidence: vec![
"Current HEAD is already reflected in the workspace-local handoff or no repo-state delta was detected."
.to_owned(),
],
},
}
}
fn build_memory_bucket(inputs: &EvaluationInputs<'_>) -> EvaluationBucket<MemoryStatus> {
if !inputs.candidates.memory.is_empty() {
return EvaluationBucket {
status: MemoryStatus::ReviewRequired,
summary:
"Structured memory entries touched this session may belong in a higher memory scope; review them before wrap-up."
.to_owned(),
evidence: inputs
.candidates
.memory
.iter()
.map(|candidate| candidate.summary.clone())
.collect(),
};
}
if inputs.effective_memory.content.is_empty() {
return EvaluationBucket {
status: MemoryStatus::NoCliSignal,
summary:
"No effective CCD-local memory is recorded for this profile, linked project, active work stream, or workspace."
.to_owned(),
evidence: vec![
"Persist durable lessons deliberately in profile, project, work stream, or workspace memory when a pattern repeats."
.to_owned(),
],
};
}
EvaluationBucket {
status: MemoryStatus::Loaded,
summary:
"Effective CCD-local memory is loaded for this profile, linked project, active work stream, or workspace."
.to_owned(),
evidence: inputs.effective_memory.contributing_paths.clone(),
}
}
fn build_wrap_up_bucket(inputs: &EvaluationInputs<'_>) -> EvaluationBucket<WrapUpStatus> {
if inputs.escalation_view.blocking_count > 0 {
return EvaluationBucket {
status: WrapUpStatus::NeedsReview,
summary:
"Blocking escalation(s) are active; resolve them before using Radar wrap-up as a clean close."
.to_owned(),
evidence: inputs
.escalation_view
.entries
.iter()
.filter(|e| matches!(e.kind, crate::state::escalation::EscalationKind::Blocking))
.map(|e| format!("{}: {}", e.id, e.reason))
.collect(),
};
}
if inputs.repo_native_checks.failures > 0 {
return EvaluationBucket {
status: WrapUpStatus::NeedsReview,
summary:
"One or more repo-native checks failed; resolve them before using Radar wrap-up as a clean close."
.to_owned(),
evidence: inputs
.repo_native_checks
.checks
.iter()
.filter(|check| check.severity == "error")
.map(|check| format!("{}: {}", check.id, check.message))
.collect(),
};
}
if inputs.behavioral_drift.status == DriftAggregateStatus::NeedsRecalibration {
return EvaluationBucket {
status: WrapUpStatus::NeedsReview,
summary:
"Behavioral drift was detected; recalibrate the handoff or focus before using Radar wrap-up as a clean close."
.to_owned(),
evidence: inputs.behavioral_drift.evidence.clone(),
};
}
if inputs.git.is_some_and(|git| git.behind > 0) {
return EvaluationBucket {
status: WrapUpStatus::NeedsReview,
summary:
"The branch is behind its upstream; reconcile before using Radar wrap-up as a clean close."
.to_owned(),
evidence: vec![format!(
"Behind upstream by {} commit(s).",
inputs.git.expect("checked above").behind
)],
};
}
if inputs.git.is_some_and(|git| !git.clean) {
return EvaluationBucket {
status: WrapUpStatus::ReadyToCommit,
summary:
"Local changes are present; the repo is ready for validation, review, and commit."
.to_owned(),
evidence: wrap_up_evidence(inputs.git.expect("checked above")),
};
}
if inputs.git.is_some_and(|git| git.ahead > 0) {
return EvaluationBucket {
status: WrapUpStatus::ReadyToPush,
summary: "No local file changes remain and local commits are ready to push.".to_owned(),
evidence: vec![format!(
"Ahead of upstream by {} commit(s).",
inputs.git.expect("checked above").ahead
)],
};
}
if inputs.git.is_none() {
return EvaluationBucket {
status: WrapUpStatus::Clean,
summary:
"Git checkout context is unavailable by design for directory-substrate workspaces."
.to_owned(),
evidence: vec![
"Wrap-up review is limited to continuity, memory, execution gates, and repo-native checks.".to_owned(),
],
};
}
EvaluationBucket {
status: WrapUpStatus::Clean,
summary: "No local changes or unpublished commits were detected.".to_owned(),
evidence: vec!["Worktree is clean and branch is in sync with upstream.".to_owned()],
}
}
fn execution_gate_evidence(view: &session_gates::ExecutionGatesView) -> Vec<String> {
let mut evidence = Vec::new();
if let Some(seed) = &view.seeded_from {
evidence.push(format!("Gate set source: {seed}."));
}
if let Some(anchor) = &view.attention_anchor {
evidence.push(format!(
"Current gate [{}/{} {}]: {}",
anchor.index,
view.total_count,
anchor.status.as_str(),
anchor.text
));
}
if view.unfinished_count > 0 {
evidence.push(format!(
"{} execution gate(s) remain unfinished.",
view.unfinished_count
));
}
evidence
}
fn wrap_up_evidence(git: &GitState) -> Vec<String> {
let mut evidence = Vec::new();
if !git.staged_files.is_empty() {
evidence.push(format!("Staged files: {}", git.staged_files.join(", ")));
}
if !git.unstaged_files.is_empty() {
evidence.push(format!("Unstaged files: {}", git.unstaged_files.join(", ")));
}
if !git.untracked_files.is_empty() {
evidence.push(format!(
"Untracked files: {}",
git.untracked_files.join(", ")
));
}
evidence
}