use std::collections::BTreeSet;
use std::path::Path;
use crate::memory::{entries as memory_entries, promote as memory_promote};
use crate::state::session as session_state;
use super::{
active_session_start_count, CandidateAction, CandidateActionApply, CandidateUpdate,
CandidateUpdates, MemoryState,
};
pub(super) fn build_candidate_updates(
repo_root: &Path,
profile: &str,
_handoff: &super::HandoffState,
effective_memory: &MemoryState,
tracked_session: Option<&session_state::SessionStateFile>,
branch_memory_available: bool,
) -> CandidateUpdates {
CandidateUpdates {
handoff: Vec::new(),
memory: build_memory_candidates(
repo_root,
profile,
effective_memory,
tracked_session,
branch_memory_available,
),
}
}
#[derive(Clone, Copy)]
enum MemoryCandidateScope {
Clone,
Branch,
Pod,
}
impl MemoryCandidateScope {
fn label(self) -> &'static str {
match self {
Self::Clone => "workspace",
Self::Branch => "work_stream",
Self::Pod => "pod",
}
}
fn source_scope(self) -> &'static str {
match self {
Self::Clone => "workspace-memory",
Self::Branch => "work-stream-memory",
Self::Pod => "pod-memory",
}
}
fn suggested_destination(self, branch_memory_available: bool) -> &'static str {
match self {
Self::Clone if branch_memory_available => "work-stream-memory",
Self::Clone | Self::Branch | Self::Pod => "project-memory",
}
}
fn promotion_review_summary(self, entry: &memory_entries::StructuredMemoryEntry) -> String {
match self {
Self::Clone => format!(
"{} memory entry `{}` was touched this session; review whether it should stay workspace-local or be promoted to a higher memory scope.",
self.label(),
entry.id
),
Self::Branch | Self::Pod => format!(
"{} memory entry `{}` was touched this session; review whether it should move into project memory.",
self.label(),
entry.id
),
}
}
}
fn build_memory_candidates(
repo_root: &Path,
profile: &str,
effective_memory: &MemoryState,
tracked_session: Option<&session_state::SessionStateFile>,
branch_memory_available: bool,
) -> Vec<CandidateUpdate> {
let Some(current_session_start_count) = active_session_start_count(tracked_session) else {
return Vec::new();
};
let branch_entry_ids = effective_memory
.structured
.branch_entries
.iter()
.map(|entry| entry.id.clone())
.collect::<BTreeSet<_>>();
let repo_entry_ids = effective_memory
.structured
.repo_entries
.iter()
.map(|entry| entry.id.clone())
.collect::<BTreeSet<_>>();
let context = MemoryCandidateContext {
repo_root,
profile,
branch_entry_ids: &branch_entry_ids,
repo_entry_ids: &repo_entry_ids,
current_session_start_count,
branch_memory_available,
};
let mut memory = Vec::new();
extend_memory_candidates(
&mut memory,
&effective_memory.structured.clone_entries,
MemoryCandidateScope::Clone,
&context,
);
extend_memory_candidates(
&mut memory,
&effective_memory.structured.branch_entries,
MemoryCandidateScope::Branch,
&context,
);
extend_memory_candidates(
&mut memory,
&effective_memory.structured.pod_entries,
MemoryCandidateScope::Pod,
&context,
);
memory
}
struct MemoryCandidateContext<'a> {
repo_root: &'a Path,
profile: &'a str,
branch_entry_ids: &'a BTreeSet<String>,
repo_entry_ids: &'a BTreeSet<String>,
current_session_start_count: u64,
branch_memory_available: bool,
}
fn extend_memory_candidates(
candidates: &mut Vec<CandidateUpdate>,
entries: &[memory_entries::StructuredMemoryEntry],
scope: MemoryCandidateScope,
context: &MemoryCandidateContext<'_>,
) {
candidates.extend(
entries
.iter()
.filter_map(|entry| memory_candidate_from_entry(entry, scope, context)),
);
}
fn memory_candidate_from_entry(
entry: &memory_entries::StructuredMemoryEntry,
scope: MemoryCandidateScope,
context: &MemoryCandidateContext<'_>,
) -> Option<CandidateUpdate> {
if entry.state != "active"
|| entry.last_touched_session != context.current_session_start_count
|| entry.source_ref.is_some()
|| !memory_promote::is_promotable_entry_type(&entry.entry_type)
{
return None;
}
let suggested_destination = scope.suggested_destination(context.branch_memory_available);
if candidate_already_exists(
&entry.id,
suggested_destination,
context.branch_entry_ids,
context.repo_entry_ids,
) {
return None;
}
Some(CandidateUpdate {
kind: "promotion_review",
summary: scope.promotion_review_summary(entry),
section: None,
replacement_lines: None,
entry_id: Some(entry.id.clone()),
source_scope: Some(scope.source_scope()),
suggested_destination: Some(suggested_destination),
action: Some(memory_candidate_admit_preview_action(
context.repo_root,
context.profile,
entry.id.as_str(),
scope,
suggested_destination,
)),
})
}
fn candidate_already_exists(
entry_id: &str,
suggested_destination: &'static str,
branch_entry_ids: &BTreeSet<String>,
repo_entry_ids: &BTreeSet<String>,
) -> bool {
match suggested_destination {
"work-stream-memory" => {
branch_entry_ids.contains(entry_id) || repo_entry_ids.contains(entry_id)
}
"project-memory" => repo_entry_ids.contains(entry_id),
_ => false,
}
}
fn memory_candidate_admit_preview_action(
repo_root: &Path,
profile: &str,
entry_id: &str,
scope: MemoryCandidateScope,
suggested_destination: &'static str,
) -> CandidateAction {
CandidateAction {
kind: "memory_candidate_admit_preview",
argv: vec![
"ccd".to_owned(),
"memory".to_owned(),
"candidate".to_owned(),
"admit".to_owned(),
"--path".to_owned(),
repo_root.display().to_string(),
"--profile".to_owned(),
profile.to_owned(),
"--entry".to_owned(),
entry_id.to_owned(),
"--source-scope".to_owned(),
scope.source_scope().to_owned(),
"--destination".to_owned(),
suggested_destination.to_owned(),
],
note: "Preview only. Add `--write` to stage this reviewed candidate as a higher-scope `promotion_candidate` entry.",
apply: Some(memory_candidate_admit_apply_action(
repo_root,
profile,
entry_id,
scope,
suggested_destination,
)),
}
}
fn memory_candidate_admit_apply_action(
repo_root: &Path,
profile: &str,
entry_id: &str,
scope: MemoryCandidateScope,
suggested_destination: &'static str,
) -> CandidateActionApply {
CandidateActionApply {
argv: vec![
"ccd".to_owned(),
"memory".to_owned(),
"candidate".to_owned(),
"admit".to_owned(),
"--path".to_owned(),
repo_root.display().to_string(),
"--profile".to_owned(),
profile.to_owned(),
"--entry".to_owned(),
entry_id.to_owned(),
"--source-scope".to_owned(),
scope.source_scope().to_owned(),
"--destination".to_owned(),
suggested_destination.to_owned(),
"--write".to_owned(),
],
requirements: Vec::new(),
note: "Mutates memory surfaces. Execute this apply command only after reviewing the staged candidate.",
}
}