use std::path::Path;
use std::process::ExitCode;
use anyhow::{bail, Result};
use serde::Serialize;
use tracing::debug;
use crate::memory::entries::StructuredMemoryEntry;
use crate::memory::file::{append_block_to_contents, render_entry_block};
use crate::memory::{governance, promote, queue};
use crate::output::CommandReport;
use crate::paths::state::StateLayout;
use crate::profile;
use crate::repo::marker as repo_marker;
use crate::repo::registry as repo_registry;
use crate::state::protected_write::ExclusiveWriteOptions;
use crate::state::runtime::{
self as runtime_state, LoadedRuntimeMemorySurface, RuntimeMemoryEntry,
};
use crate::state::session;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CandidateSourceScope {
Clone,
Branch,
}
impl CandidateSourceScope {
fn cli_label(self) -> &'static str {
match self {
Self::Clone => "workspace-memory",
Self::Branch => "work-stream-memory",
}
}
fn report_scope(self) -> &'static str {
match self {
Self::Clone => "workspace_memory",
Self::Branch => "work_stream_memory",
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CandidateDestination {
Branch,
Repo,
}
impl CandidateDestination {
fn cli_label(self) -> &'static str {
match self {
Self::Branch => "work-stream-memory",
Self::Repo => "project-memory",
}
}
fn report_scope(self) -> &'static str {
match self {
Self::Branch => "work_stream_memory",
Self::Repo => "project_memory",
}
}
}
#[derive(Serialize)]
pub struct MemoryCandidateAdmitReport {
command: &'static str,
ok: bool,
path: String,
profile: String,
project_id: String,
locality_id: String,
mode: &'static str,
source: MemorySurfaceView,
destination: MemorySurfaceView,
selected_entry: StructuredMemoryEntry,
destination_preview: DestinationPreviewView,
next_action: NextActionView,
governance: governance::GovernanceDecisionView,
#[serde(skip_serializing_if = "Option::is_none")]
write_result: Option<WriteResultView>,
#[serde(skip_serializing_if = "Option::is_none")]
staged_write: Option<queue::StagedMemoryOpView>,
#[serde(skip_serializing_if = "Option::is_none")]
message: Option<String>,
warnings: Vec<String>,
}
#[derive(Serialize)]
struct MemorySurfaceView {
scope: &'static str,
path: String,
status: &'static str,
}
#[derive(Serialize)]
struct DestinationPreviewView {
append_mode: &'static str,
file_status: &'static str,
provenance_link: String,
entry_block: String,
note: String,
}
#[derive(Serialize)]
struct NextActionView {
kind: &'static str,
argv: Vec<String>,
note: &'static str,
}
#[derive(Serialize)]
struct WriteResultView {
destination_action: &'static str,
destination_path: String,
compiled_state_action: &'static str,
written_destination_entry: StructuredMemoryEntry,
}
impl CommandReport for MemoryCandidateAdmitReport {
fn exit_code(&self) -> ExitCode {
if self.ok {
ExitCode::SUCCESS
} else {
ExitCode::from(1)
}
}
fn render_text(&self) {
if !self.ok {
println!(
"{}",
self.message
.as_deref()
.unwrap_or("Queued memory candidate write was rejected.")
);
return;
}
let destination_scope = self.destination.scope.replace('_', " ");
match &self.write_result {
Some(_) => println!(
"Admitted reviewed memory candidate {} into {}.",
self.selected_entry.id, destination_scope
),
None => println!(
"Prepared memory candidate admission preview for entry {}.",
self.selected_entry.id
),
}
println!("Mode: {}", self.mode);
println!("Source: {} ({})", self.source.path, self.source.status);
println!(
"Destination: {} ({})",
self.destination.path, self.destination.status
);
println!("Entry type: {}", self.selected_entry.entry_type);
println!(
"Destination provenance: {}",
self.destination_preview.provenance_link
);
println!(
"Governance: {} ({})",
self.governance.action.as_str(),
self.governance.status.as_str()
);
if let Some(write_result) = &self.write_result {
println!(
"Destination write: {} ({})",
write_result.destination_action, write_result.destination_path
);
println!(
"Compiled state: {}",
write_result.compiled_state_action.replace('_', " ")
);
}
if let Some(staged_write) = &self.staged_write {
println!(
"Queued op: {} ({})",
staged_write.op_id,
match staged_write.outcome {
crate::state::protected_write::AppendWriteOutcome::Applied => "applied",
crate::state::protected_write::AppendWriteOutcome::IdempotentNoop => {
"idempotent_noop"
}
crate::state::protected_write::AppendWriteOutcome::OwnershipConflict => {
"ownership_conflict"
}
crate::state::protected_write::AppendWriteOutcome::StaleSession => {
"stale_session"
}
crate::state::protected_write::AppendWriteOutcome::UnsupportedMultiwriter => {
"unsupported_multiwriter"
}
crate::state::protected_write::AppendWriteOutcome::DuplicateIdConflict => {
"duplicate_id_conflict"
}
}
);
}
for warning in &self.warnings {
println!("Warning: {warning}");
}
println!();
println!("Destination block:");
println!("{}", self.destination_preview.entry_block);
println!();
println!("{}", self.destination_preview.note);
println!();
println!("Next review command:");
println!("{}", self.next_action.argv.join(" "));
println!("{}", self.next_action.note);
}
}
#[allow(clippy::too_many_arguments)]
pub fn run_admit(
repo_root: &Path,
explicit_profile: Option<&str>,
entry_id: &str,
source_scope: CandidateSourceScope,
destination: CandidateDestination,
write: bool,
write_options: ExclusiveWriteOptions,
op_id_hint: Option<&str>,
) -> Result<MemoryCandidateAdmitReport> {
validate_route(source_scope, destination)?;
let profile = profile::resolve(explicit_profile)?;
let layout = StateLayout::resolve(repo_root, profile.clone())?;
ensure_profile_exists(&layout)?;
let marker = repo_marker::load(repo_root)?.ok_or_else(|| {
anyhow::anyhow!(
"repo 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 locality_id = marker.locality_id;
ensure_repo_registry_exists(&layout, repo_root, &locality_id)?;
ensure_repo_overlay_exists(&layout, &locality_id)?;
let staged_request = if should_stage_via_queue(write, destination, &layout)? {
let request_key = CandidateQueueRequestKey {
command: "memory-candidate-admit",
entry_id,
source_scope: source_scope.report_scope(),
destination: destination.report_scope(),
};
let request_fingerprint = queue::fingerprint(&request_key)?;
let op_id = queue::stable_op_id("candidate_admit", &request_fingerprint, op_id_hint);
Some((request_fingerprint, op_id))
} else {
None
};
let source_memory = load_source_surface(repo_root, &layout, &locality_id, source_scope)?;
let (selected_runtime_entry, selected_entry) = promote::load_selected_entry(
entry_id,
&source_memory,
source_scope.cli_label(),
"`ccd memory candidate admit`",
)?;
validate_candidate_source(&selected_entry)?;
let destination_memory =
load_destination_surface(repo_root, &layout, &locality_id, destination)?;
if destination_memory
.entries
.iter()
.any(|entry| entry.structured_id() == Some(selected_entry.id.as_str()))
{
bail!(
"{} already contains entry id `{}` at {}; refusing to write or preview an unexpected overwrite",
destination.cli_label(),
selected_entry.id,
destination_memory.source.path.display()
);
}
debug!(
source = source_scope.cli_label(),
destination = destination.cli_label(),
path = %destination_memory.source.path.display(),
write,
"admitting reviewed memory candidate"
);
let provenance_link = format!(
"{}#{}",
source_memory.source.path.display(),
selected_entry.id
);
let autonomous = queue::queue_required_for_autonomous(&layout)?;
let active_session_id = if autonomous {
session::load_session_id(&layout)?
} else {
None
};
let repo_overlap_memory = if destination == CandidateDestination::Branch {
Some(runtime_state::load_locality_memory_surface(
&layout,
&locality_id,
)?)
} else {
None
};
let mut governance = candidate_governance_decision(CandidateGovernanceContext {
layout: &layout,
source_scope,
destination,
selected_entry: &selected_entry,
destination_entries: &destination_memory.entries,
repo_overlap_entries: repo_overlap_memory
.as_ref()
.map(|surface| surface.entries.as_slice()),
autonomous,
session_id: active_session_id.as_deref(),
current_op_id: staged_request.as_ref().map(|(_, op_id)| op_id.as_str()),
})?;
let written_destination_runtime =
admitted_destination_entry(&selected_runtime_entry, &provenance_link);
let written_destination_entry = written_destination_runtime
.as_structured_entry()
.expect("admitted destination entry");
let destination_preview = DestinationPreviewView {
append_mode: "append_block",
file_status: destination_memory.source.status.as_str(),
provenance_link,
entry_block: render_entry_block(&written_destination_entry),
note: candidate_preview_note(write, autonomous, &governance),
};
let next_action = next_promotion_action(repo_root, profile.as_str(), entry_id, destination);
let next_destination_contents = append_block_to_contents(
&destination_memory.source.content,
&destination_preview.entry_block,
);
let write_result = write.then(|| WriteResultView {
destination_action: "append_block",
destination_path: destination_memory.source.path.display().to_string(),
compiled_state_action: "refreshed",
written_destination_entry: written_destination_entry.clone(),
});
let mut report = MemoryCandidateAdmitReport {
command: "memory-candidate-admit",
ok: true,
path: repo_root.display().to_string(),
profile: profile.to_string(),
project_id: locality_id.to_owned(),
locality_id: locality_id.to_owned(),
mode: mode_label(source_scope, destination, write),
source: MemorySurfaceView {
scope: source_scope.report_scope(),
path: source_memory.source.path.display().to_string(),
status: source_memory.source.status.as_str(),
},
destination: MemorySurfaceView {
scope: destination.report_scope(),
path: destination_memory.source.path.display().to_string(),
status: destination_memory.source.status.as_str(),
},
selected_entry,
destination_preview,
next_action,
governance: governance.clone(),
write_result,
staged_write: None,
message: None,
warnings: Vec::new(),
};
if write {
let mutation_id = staged_request
.as_ref()
.map(|(_, op_id)| op_id.as_str())
.unwrap_or("memory-candidate-admit-direct");
governance = governance.with_rollback(
mutation_id,
write_options.actor_id.as_deref(),
write_options
.session_id
.as_deref()
.or(active_session_id.as_deref()),
vec![report.selected_entry.id.clone()],
&destination_memory.source.content,
None,
);
report.governance = governance.clone();
}
if write && governance.blocked_for_write(autonomous) {
report.ok = false;
report.write_result = None;
report.message = Some(governance.blocked_write_message());
return Ok(report);
}
if let Some((request_fingerprint, op_id)) = staged_request {
let plan = queue::MemoryOpPlan {
target: queue::MemoryFileMutation {
scope: destination.report_scope().to_owned(),
path: destination_memory.source.path.display().to_string(),
next_contents: next_destination_contents,
},
source: None,
refresh_locality_id: Some(locality_id.clone()),
authored_entry_ids: vec![report.selected_entry.id.clone()],
governance: Some(governance),
};
let staged_write = queue::stage_and_reconcile(
repo_root,
&layout,
"memory-candidate-admit",
&op_id,
&request_fingerprint,
&write_options,
&plan,
)?;
let staged_ok = matches!(
staged_write.outcome,
crate::state::protected_write::AppendWriteOutcome::Applied
| crate::state::protected_write::AppendWriteOutcome::IdempotentNoop
);
if !staged_ok {
report.ok = false;
report.write_result = None;
report.message = staged_write.message.clone();
}
report.staged_write = Some(staged_write);
if report.ok {
queue::persist_report_snapshot(&layout, &op_id, &report)?;
}
} else if write {
let outcome = queue::apply_direct_plan(
repo_root,
&layout,
&queue::MemoryOpPlan {
target: queue::MemoryFileMutation {
scope: destination.report_scope().to_owned(),
path: destination_memory.source.path.display().to_string(),
next_contents: next_destination_contents,
},
source: None,
refresh_locality_id: Some(locality_id.clone()),
authored_entry_ids: vec![report.selected_entry.id.clone()],
governance: Some(governance),
},
)?;
if outcome == crate::state::protected_write::AppendWriteOutcome::IdempotentNoop {
if let Some(write_result) = report.write_result.as_mut() {
write_result.compiled_state_action = "unchanged";
}
}
}
Ok(report)
}
fn validate_route(
source_scope: CandidateSourceScope,
destination: CandidateDestination,
) -> Result<()> {
match (source_scope, destination) {
(CandidateSourceScope::Clone, CandidateDestination::Branch)
| (CandidateSourceScope::Clone, CandidateDestination::Repo)
| (CandidateSourceScope::Branch, CandidateDestination::Repo) => Ok(()),
(CandidateSourceScope::Branch, CandidateDestination::Branch) => bail!(
"`ccd memory candidate admit --source-scope work-stream-memory` only supports `--destination project-memory`"
),
}
}
fn validate_candidate_source(selected_entry: &StructuredMemoryEntry) -> Result<()> {
if selected_entry.state != "active" {
bail!(
"entry `{}` has state `{}`; `ccd memory candidate admit` only accepts `active` source entries in this first slice",
selected_entry.id,
selected_entry.state
);
}
if !promote::is_promotable_entry_type(&selected_entry.entry_type) {
bail!(
"entry `{}` has type `{}`, which is viewable but not admit-ready in the first cut",
selected_entry.id,
selected_entry.entry_type
);
}
Ok(())
}
fn load_source_surface(
repo_root: &Path,
layout: &StateLayout,
locality_id: &str,
scope: CandidateSourceScope,
) -> Result<LoadedRuntimeMemorySurface> {
match scope {
CandidateSourceScope::Clone => runtime_state::load_clone_memory_surface(layout),
CandidateSourceScope::Branch => {
if runtime_state::resolve_active_branch_memory_path(repo_root, layout, locality_id)?
.is_none()
{
bail!(
"work-stream memory is only available on active non-trunk named branches; switch to a feature branch before using `ccd memory candidate admit --source-scope work-stream-memory`"
);
}
runtime_state::load_branch_memory_surface(repo_root, layout, locality_id)
}
}
}
fn load_destination_surface(
repo_root: &Path,
layout: &StateLayout,
locality_id: &str,
destination: CandidateDestination,
) -> Result<LoadedRuntimeMemorySurface> {
match destination {
CandidateDestination::Branch => {
if runtime_state::resolve_active_branch_memory_path(repo_root, layout, locality_id)?
.is_none()
{
bail!(
"work-stream memory is only available on active non-trunk named branches; switch to a feature branch before using `ccd memory candidate admit --destination work-stream-memory`"
);
}
runtime_state::load_branch_memory_surface(repo_root, layout, locality_id)
}
CandidateDestination::Repo => {
runtime_state::load_locality_memory_surface(layout, locality_id)
}
}
}
fn admitted_destination_entry(
selected_entry: &RuntimeMemoryEntry,
provenance_link: &str,
) -> RuntimeMemoryEntry {
selected_entry
.with_structured_state("promotion_candidate")
.with_source_ref(Some(provenance_link.to_owned()))
}
fn next_promotion_action(
repo_root: &Path,
profile: &str,
entry_id: &str,
destination: CandidateDestination,
) -> NextActionView {
let mut argv = vec![
"ccd".to_owned(),
"memory".to_owned(),
"promote".to_owned(),
"--path".to_owned(),
repo_root.display().to_string(),
"--profile".to_owned(),
profile.to_owned(),
"--entry".to_owned(),
entry_id.to_owned(),
"--destination".to_owned(),
];
let (destination_arg, note) = match destination {
CandidateDestination::Branch => (
"project-memory",
"Review promotion from work-stream memory into project memory once this candidate proves durable beyond the work stream.",
),
CandidateDestination::Repo => (
"profile-memory",
"Review promotion from project memory into profile memory when this candidate proves reusable beyond the project.",
),
};
argv.push(destination_arg.to_owned());
NextActionView {
kind: "memory_promote_preview",
argv,
note,
}
}
fn mode_label(
source_scope: CandidateSourceScope,
destination: CandidateDestination,
write: bool,
) -> &'static str {
match (source_scope, destination, write) {
(CandidateSourceScope::Clone, CandidateDestination::Branch, false) => {
"workspace_memory_to_work_stream_candidate_preview"
}
(CandidateSourceScope::Clone, CandidateDestination::Branch, true) => {
"workspace_memory_to_work_stream_candidate_write"
}
(CandidateSourceScope::Branch, CandidateDestination::Repo, false) => {
"work_stream_memory_to_project_candidate_preview"
}
(CandidateSourceScope::Branch, CandidateDestination::Repo, true) => {
"work_stream_memory_to_project_candidate_write"
}
(_, CandidateDestination::Repo, false) => "workspace_memory_to_project_candidate_preview",
(_, CandidateDestination::Repo, true) => "workspace_memory_to_project_candidate_write",
_ => unreachable!("invalid source/destination route already rejected"),
}
}
fn should_stage_via_queue(
write: bool,
destination: CandidateDestination,
layout: &StateLayout,
) -> Result<bool> {
if !write {
return Ok(false);
}
let _ = destination;
queue::queue_required_for_autonomous(layout)
}
struct CandidateGovernanceContext<'a> {
layout: &'a StateLayout,
source_scope: CandidateSourceScope,
destination: CandidateDestination,
selected_entry: &'a StructuredMemoryEntry,
destination_entries: &'a [RuntimeMemoryEntry],
repo_overlap_entries: Option<&'a [RuntimeMemoryEntry]>,
autonomous: bool,
session_id: Option<&'a str>,
current_op_id: Option<&'a str>,
}
fn candidate_governance_decision(
context: CandidateGovernanceContext<'_>,
) -> Result<governance::GovernanceDecisionView> {
if let Some(duplicate) =
governance::find_runtime_duplicate(context.destination_entries, context.selected_entry)
{
return Ok(governance::GovernanceDecisionView::new(
governance::GovernanceDecisionInit {
action: governance::GovernanceAction::Discard,
status: governance::GovernanceStatus::Discarded,
target_scope: context.destination.report_scope().to_owned(),
source_scope: Some(context.source_scope.report_scope().to_owned()),
entry_type: context.selected_entry.entry_type.clone(),
rationale: format!(
"a normalized duplicate already exists in {} as `{}`",
context.destination.report_scope(),
duplicate.entry_id
),
evidence_summary: vec![
"duplicate suppression applies before widening scope".to_owned(),
"the destination already carries the same normalized lesson".to_owned(),
],
pressure_signals: vec![governance::signal(
"duplicate_match",
format!(
"{} already contains normalized duplicate `{}`",
context.destination.report_scope(),
duplicate.entry_id
),
)],
duplicate_digest: Some(duplicate.digest),
rate_limit: None,
},
));
}
if context.destination == CandidateDestination::Branch {
if let Some(repo_entries) = context.repo_overlap_entries {
if let Some(duplicate) =
governance::find_runtime_duplicate(repo_entries, context.selected_entry)
{
return Ok(governance::GovernanceDecisionView::new(
governance::GovernanceDecisionInit {
action: governance::GovernanceAction::Discard,
status: governance::GovernanceStatus::Discarded,
target_scope: context.destination.report_scope().to_owned(),
source_scope: Some(context.source_scope.report_scope().to_owned()),
entry_type: context.selected_entry.entry_type.clone(),
rationale: format!(
"work-stream capture overlaps existing project memory entry `{}`",
duplicate.entry_id
),
evidence_summary: vec![
"work-stream capture stays local when project memory already contains the lesson"
.to_owned(),
],
pressure_signals: vec![governance::signal(
"broader_scope_overlap",
format!(
"project_memory already carries normalized duplicate `{}`",
duplicate.entry_id
),
)],
duplicate_digest: Some(duplicate.digest),
rate_limit: None,
},
));
}
}
}
let action = match context.destination {
CandidateDestination::Branch => governance::GovernanceAction::AdmitWorkStream,
CandidateDestination::Repo => governance::GovernanceAction::StageProjectPromotion,
};
let rate_limit = if context.autonomous {
governance::session_rate_limit(
context.layout,
context.session_id,
context.destination.report_scope(),
context.current_op_id,
)?
} else {
None
};
let mut pressure_signals = Vec::new();
if let Some(rate_limit) = &rate_limit {
if rate_limit.remaining == 0 {
pressure_signals.push(governance::signal(
"session_cap_reached",
format!(
"{} already has {} queued admissions or promotions in this session",
rate_limit.scope, rate_limit.count
),
));
} else if rate_limit.remaining == 1 {
pressure_signals.push(governance::signal(
"session_cap_near",
format!(
"{} has one remaining governed mutation slot in this session",
rate_limit.scope
),
));
}
}
let status = if rate_limit
.as_ref()
.is_some_and(|limit| limit.status == "reached")
{
governance::GovernanceStatus::RateLimited
} else {
governance::GovernanceStatus::Allowed
};
let rationale = match action {
governance::GovernanceAction::AdmitWorkStream => {
"work-stream admission is allowed for a focused lesson on an active non-trunk work stream"
.to_owned()
}
governance::GovernanceAction::StageProjectPromotion => {
"project-scope admission stays staged as a governed promotion candidate".to_owned()
}
_ => unreachable!("candidate admit only targets work-stream or project memory"),
};
Ok(governance::GovernanceDecisionView::new(
governance::GovernanceDecisionInit {
action,
status,
target_scope: context.destination.report_scope().to_owned(),
source_scope: Some(context.source_scope.report_scope().to_owned()),
entry_type: context.selected_entry.entry_type.clone(),
rationale,
evidence_summary: vec![
format!(
"entry type `{}` is admit-ready",
context.selected_entry.entry_type
),
format!(
"source scope `{}` was validated",
context.source_scope.report_scope()
),
format!(
"destination scope `{}` remains bounded",
context.destination.report_scope()
),
],
pressure_signals,
duplicate_digest: Some(governance::normalized_duplicate_digest(
context.selected_entry,
)),
rate_limit,
},
))
}
fn candidate_preview_note(
write: bool,
autonomous: bool,
decision: &governance::GovernanceDecisionView,
) -> String {
if write && decision.blocked_for_write(autonomous) {
return format!(
"Write was not applied. Governance outcome: {} ({})",
decision.action.as_str(),
decision.blocked_write_message()
);
}
if !write && decision.status != governance::GovernanceStatus::Allowed {
return format!(
"Preview only. Governance outcome: {} ({})",
decision.action.as_str(),
decision.rationale
);
}
if write {
"Applied with explicit approval. The block above was appended as a higher-scope `promotion_candidate` entry.".to_owned()
} else {
"Preview only. No files were changed. Rerun with `--write` to stage this reviewed candidate in the destination memory scope.".to_owned()
}
}
#[derive(Serialize)]
struct CandidateQueueRequestKey<'a> {
command: &'static str,
entry_id: &'a str,
source_scope: &'static str,
destination: &'static str,
}
pub(crate) fn replay_request(
entry_id: &str,
source_scope: CandidateSourceScope,
destination: CandidateDestination,
op_id_hint: Option<&str>,
) -> Result<(String, String)> {
let request_key = CandidateQueueRequestKey {
command: "memory-candidate-admit",
entry_id,
source_scope: source_scope.report_scope(),
destination: destination.report_scope(),
};
let request_fingerprint = queue::fingerprint(&request_key)?;
let op_id = queue::stable_op_id("candidate_admit", &request_fingerprint, op_id_hint);
Ok((request_fingerprint, op_id))
}
fn ensure_profile_exists(layout: &StateLayout) -> Result<()> {
let profile_root = layout.profile_root();
if profile_root.is_dir() {
return Ok(());
}
bail!(
"profile `{}` does not exist at {}; bootstrap it with `ccd attach` before using `ccd memory candidate admit`",
layout.profile(),
profile_root.display()
)
}
fn ensure_repo_registry_exists(
layout: &StateLayout,
repo_root: &Path,
locality_id: &str,
) -> Result<()> {
let registry_path = layout.repo_metadata_path(locality_id)?;
match repo_registry::load(®istry_path)? {
Some(_) => Ok(()),
None => bail!(
"repo registry is missing for locality_id `{}` at {}; run `ccd link --path {}` or `ccd attach --path {}` again",
locality_id,
registry_path.display(),
repo_root.display(),
repo_root.display()
),
}
}
fn ensure_repo_overlay_exists(layout: &StateLayout, locality_id: &str) -> Result<()> {
let overlay_root = layout.repo_overlay_root(locality_id)?;
if overlay_root.is_dir() {
return Ok(());
}
bail!(
"repo overlay for `{}` is missing at {}; run `ccd attach --path .` or `ccd link --path .` to restore it before using `ccd memory candidate admit`",
locality_id,
overlay_root.display()
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::entries::StructuredMemoryEntry;
fn structured_entry(state: &str, entry_type: &str) -> StructuredMemoryEntry {
StructuredMemoryEntry {
id: "entry-1".to_owned(),
entry_type: entry_type.to_owned(),
state: state.to_owned(),
created_at: "2024-01-01T00:00:00Z".to_owned(),
last_touched_session: 1,
origin: "manual".to_owned(),
superseded_at: None,
decay_class: None,
expires_at: None,
tags: Vec::new(),
source_ref: None,
supersedes: Vec::new(),
content: "A lesson worth keeping".to_owned(),
}
}
#[test]
fn validate_route_rejects_branch_to_branch_admission() {
let error = validate_route(CandidateSourceScope::Branch, CandidateDestination::Branch)
.expect_err("branch-to-branch route should fail");
assert!(error.to_string().contains("project-memory"));
}
#[test]
fn validate_candidate_source_rejects_inactive_entries() {
let error = validate_candidate_source(&structured_entry("superseded", "rule"))
.expect_err("inactive source entries should fail");
assert!(error.to_string().contains("active"));
}
}